@allurereport/reader 3.0.0-beta.10
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 +25 -0
- package/dist/allure1/index.d.ts +2 -0
- package/dist/allure1/index.js +289 -0
- package/dist/allure2/index.d.ts +2 -0
- package/dist/allure2/index.js +306 -0
- package/dist/allure2/model.d.ts +78 -0
- package/dist/allure2/model.js +21 -0
- package/dist/attachments/index.d.ts +2 -0
- package/dist/attachments/index.js +8 -0
- package/dist/cucumberjson/index.d.ts +2 -0
- package/dist/cucumberjson/index.js +306 -0
- package/dist/cucumberjson/model.d.ts +65 -0
- package/dist/cucumberjson/model.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/junitxml/index.d.ts +2 -0
- package/dist/junitxml/index.js +158 -0
- package/dist/model.d.ts +20 -0
- package/dist/model.js +1 -0
- package/dist/properties.d.ts +1 -0
- package/dist/properties.js +229 -0
- package/dist/toolRunner.d.ts +17 -0
- package/dist/toolRunner.js +143 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +27 -0
- package/dist/validation.d.ts +45 -0
- package/dist/validation.js +51 -0
- package/dist/xcresult/bundle.d.ts +7 -0
- package/dist/xcresult/bundle.js +63 -0
- package/dist/xcresult/index.d.ts +2 -0
- package/dist/xcresult/index.js +91 -0
- package/dist/xcresult/model.d.ts +61 -0
- package/dist/xcresult/model.js +1 -0
- package/dist/xcresult/utils.d.ts +43 -0
- package/dist/xcresult/utils.js +378 -0
- package/dist/xcresult/xcresulttool/cli.d.ts +10 -0
- package/dist/xcresult/xcresulttool/cli.js +46 -0
- package/dist/xcresult/xcresulttool/index.d.ts +7 -0
- package/dist/xcresult/xcresulttool/index.js +311 -0
- package/dist/xcresult/xcresulttool/legacy/index.d.ts +10 -0
- package/dist/xcresult/xcresulttool/legacy/index.js +456 -0
- package/dist/xcresult/xcresulttool/legacy/model.d.ts +75 -0
- package/dist/xcresult/xcresulttool/legacy/model.js +1 -0
- package/dist/xcresult/xcresulttool/legacy/parsing.d.ts +15 -0
- package/dist/xcresult/xcresulttool/legacy/parsing.js +41 -0
- package/dist/xcresult/xcresulttool/legacy/utils.d.ts +7 -0
- package/dist/xcresult/xcresulttool/legacy/utils.js +33 -0
- package/dist/xcresult/xcresulttool/legacy/xcModel.d.ts +357 -0
- package/dist/xcresult/xcresulttool/legacy/xcModel.js +5 -0
- package/dist/xcresult/xcresulttool/model.d.ts +17 -0
- package/dist/xcresult/xcresulttool/model.js +6 -0
- package/dist/xcresult/xcresulttool/utils.d.ts +3 -0
- package/dist/xcresult/xcresulttool/utils.js +74 -0
- package/dist/xcresult/xcresulttool/xcModel.d.ts +96 -0
- package/dist/xcresult/xcresulttool/xcModel.js +18 -0
- package/dist/xml-utils.d.ts +5 -0
- package/dist/xml-utils.js +29 -0
- package/package.json +56 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import console from "node:console";
|
|
2
|
+
import { IS_MAC, XCRESULTTOOL_MISSING_MESSAGE, isXcResultBundle } from "./bundle.js";
|
|
3
|
+
import { version } from "./xcresulttool/cli.js";
|
|
4
|
+
import LegacyApiParser from "./xcresulttool/legacy/index.js";
|
|
5
|
+
import { parseWithExportedAttachments } from "./xcresulttool/utils.js";
|
|
6
|
+
const readerId = "xcresult";
|
|
7
|
+
export const readXcResultBundle = async (visitor, directory) => {
|
|
8
|
+
if (await isXcResultBundle(directory)) {
|
|
9
|
+
if (!IS_MAC) {
|
|
10
|
+
console.warn(`It looks like ${directory} is a Mac OS bundle. Allure 3 can only parse such bundles on a Mac OS machine.`);
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const xcResultToolVersion = await maybeGetXcResultToolVersion();
|
|
14
|
+
if (xcResultToolVersion) {
|
|
15
|
+
return await parseBundleWithXcResultTool(visitor, directory, xcResultToolVersion);
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
21
|
+
const maybeGetXcResultToolVersion = async () => {
|
|
22
|
+
try {
|
|
23
|
+
return await version();
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.error(XCRESULTTOOL_MISSING_MESSAGE);
|
|
27
|
+
console.error(e);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const parseBundleWithXcResultTool = async (visitor, xcResultPath, xcResultToolVersion) => {
|
|
31
|
+
try {
|
|
32
|
+
if (isXcode16OrNewer(xcResultToolVersion)) {
|
|
33
|
+
await parseWithXcode16OrNewer(visitor, xcResultPath, xcResultToolVersion);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
await parseWithXcode15OrOlder(visitor, xcResultPath);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.error("error parsing", xcResultPath, e);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
const parseWithXcode15OrOlder = async (visitor, xcResultPath) => {
|
|
46
|
+
const legacyApiParser = new LegacyApiParser({
|
|
47
|
+
xcResultPath,
|
|
48
|
+
xcode16Plus: false,
|
|
49
|
+
});
|
|
50
|
+
await tryApi(visitor, xcResultPath, legacyApiParser);
|
|
51
|
+
};
|
|
52
|
+
const parseWithXcode16OrNewer = async (visitor, xcResultPath, xcResultToolVersion) => {
|
|
53
|
+
await parseWithExportedAttachments(xcResultPath, async (createAttachmentFile) => {
|
|
54
|
+
const legacyApiParser = new LegacyApiParser({
|
|
55
|
+
xcResultPath,
|
|
56
|
+
createAttachmentFile,
|
|
57
|
+
xcode16Plus: true,
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
await tryApi(visitor, xcResultPath, legacyApiParser);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
console.error(e);
|
|
65
|
+
if (legacyApiParser.legacyApiSucceeded()) {
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`The legacy xcresulttool API can't be accessed in ${xcResultToolVersion}. ` +
|
|
70
|
+
"The new API is not yet supported by the reader. " +
|
|
71
|
+
"Please, see https://github.com/allure-framework/allure3/issues/110 for more details");
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const tryApi = async (visitor, originalFileName, apiParser) => {
|
|
75
|
+
for await (const x of apiParser.parse()) {
|
|
76
|
+
if ("readContent" in x) {
|
|
77
|
+
await visitor.visitAttachmentFile(x, { readerId });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
visitor.visitTestResult(x, { readerId, metadata: { originalFileName } });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const isXcode16OrNewer = (versionText) => {
|
|
85
|
+
const versionMatch = versionText.match(/xcresulttool version (\d+)/);
|
|
86
|
+
if (versionMatch) {
|
|
87
|
+
const xcResultToolVersion = parseInt(versionMatch[1], 10);
|
|
88
|
+
return xcResultToolVersion >= 23000;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RawTestLabel, RawTestLink, RawTestParameter } from "@allurereport/reader-api";
|
|
2
|
+
import type { XcTestResult } from "./xcresulttool/xcModel.js";
|
|
3
|
+
export type XcAttachments = Map<string, XcAttachmentMetadata[]>;
|
|
4
|
+
export type XcAttachmentMetadata = {
|
|
5
|
+
path: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
timestamp?: number;
|
|
8
|
+
};
|
|
9
|
+
export type TestDetailsRunData = {
|
|
10
|
+
duration?: number;
|
|
11
|
+
result?: XcTestResult;
|
|
12
|
+
parameters?: (string | undefined)[];
|
|
13
|
+
emitted?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type TestRunCoordinates = {
|
|
16
|
+
device?: string;
|
|
17
|
+
testPlan?: string;
|
|
18
|
+
attempt?: number;
|
|
19
|
+
args?: TestRunArgs;
|
|
20
|
+
};
|
|
21
|
+
export type TestRunArgs = ({
|
|
22
|
+
parameter: string;
|
|
23
|
+
value: string;
|
|
24
|
+
} | undefined)[];
|
|
25
|
+
export type TestRunSelector = {
|
|
26
|
+
device?: string;
|
|
27
|
+
testPlan?: string;
|
|
28
|
+
attempt?: number;
|
|
29
|
+
args?: (string | undefined)[];
|
|
30
|
+
};
|
|
31
|
+
export type TestRunLookup<Data> = Map<string, Map<string, Map<string, Data[]>>>;
|
|
32
|
+
export type TargetDescriptor = {
|
|
33
|
+
model?: string;
|
|
34
|
+
architecture?: string;
|
|
35
|
+
platform?: string;
|
|
36
|
+
osVersion?: string;
|
|
37
|
+
};
|
|
38
|
+
export type AllureApiCallBase<Type extends string, Value> = {
|
|
39
|
+
type: Type;
|
|
40
|
+
value: Value;
|
|
41
|
+
};
|
|
42
|
+
export type AllureNameApiCall = AllureApiCallBase<"name", string>;
|
|
43
|
+
export type AllureDescriptionApiCall = AllureApiCallBase<"description", string>;
|
|
44
|
+
export type AllurePreconditionApiCall = AllureApiCallBase<"precondition", string>;
|
|
45
|
+
export type AllureExpectedResultApiCall = AllureApiCallBase<"expectedResult", string>;
|
|
46
|
+
export type AllureLabelApiCall = AllureApiCallBase<"label", RawTestLabel>;
|
|
47
|
+
export type AllureLinkApiCall = AllureApiCallBase<"link", RawTestLink>;
|
|
48
|
+
export type AllureParameterApiCall = AllureApiCallBase<"parameter", RawTestParameter>;
|
|
49
|
+
export type AllureFlakyApiCall = AllureApiCallBase<"flaky", boolean>;
|
|
50
|
+
export type AllureMutedApiCall = AllureApiCallBase<"muted", boolean>;
|
|
51
|
+
export type AllureKnownApiCall = AllureApiCallBase<"known", boolean>;
|
|
52
|
+
export type AllureApiCall = AllureNameApiCall | AllureDescriptionApiCall | AllurePreconditionApiCall | AllureExpectedResultApiCall | AllureLabelApiCall | AllureLinkApiCall | AllureParameterApiCall | AllureFlakyApiCall | AllureMutedApiCall | AllureKnownApiCall;
|
|
53
|
+
export type LabelsInputData = {
|
|
54
|
+
hostName: string | undefined;
|
|
55
|
+
projectName: string | undefined;
|
|
56
|
+
bundle: string | undefined;
|
|
57
|
+
suites: readonly string[];
|
|
58
|
+
className: string | undefined;
|
|
59
|
+
functionName: string | undefined;
|
|
60
|
+
tags: readonly string[];
|
|
61
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RawStep, RawTestLabel, RawTestResult, RawTestStatus, RawTestStepResult } from "@allurereport/reader-api";
|
|
2
|
+
import type { Unknown } from "../validation.js";
|
|
3
|
+
import type { AllureApiCall, LabelsInputData, TargetDescriptor, TestRunArgs, TestRunCoordinates, TestRunLookup, TestRunSelector } from "./model.js";
|
|
4
|
+
export declare const MS_IN_S = 1000;
|
|
5
|
+
export declare const ALLURE_API_ACTIVITY_PREFIX = "allure.";
|
|
6
|
+
export declare const statusPriorities: Map<RawTestStatus, number>;
|
|
7
|
+
export declare const utiToMediaType: Record<string, string>;
|
|
8
|
+
export declare const getMediaTypeByUti: (uti: string | undefined) => string | undefined;
|
|
9
|
+
export declare const prependTitle: (title: string, text: string, spaces: number) => string;
|
|
10
|
+
export declare const getWorstStatusWithDetails: (failureSteps: readonly RawTestStepResult[]) => Pick<RawTestStepResult, "status" | "message" | "trace">;
|
|
11
|
+
export declare const countExpectedFailures: (failureSteps: readonly RawTestStepResult[]) => number;
|
|
12
|
+
export declare const resolveFailureMessage: (firstFailureMessage: string | undefined, failuresCount: number, expectedFailuresCount: number) => string | undefined;
|
|
13
|
+
export declare const getAggregatedFailureMessage: (message: string | undefined, failures: number, expected: number) => string;
|
|
14
|
+
export declare const DEFAULT_BUNDLE_NAME = "The test bundle name is not defined";
|
|
15
|
+
export declare const DEFAULT_SUITE_ID = "__unknown__";
|
|
16
|
+
export declare const DEFAULT_SUITE_NAME = "The test suite name is not defined";
|
|
17
|
+
export declare const DEFAULT_TEST_NAME = "The test name is not defined";
|
|
18
|
+
export declare const DEFAULT_STEP_NAME = "The test name is not defined";
|
|
19
|
+
export declare const DEFAULT_ATTACHMENT_NAME = "Attachment";
|
|
20
|
+
export declare const DEFAULT_EXPECTED_FAILURE_REASON = "Expected failure";
|
|
21
|
+
export declare const SURROGATE_DEVICE_ID: `${string}-${string}-${string}-${string}-${string}`;
|
|
22
|
+
export declare const SURROGATE_TEST_PLAN_ID: `${string}-${string}-${string}-${string}-${string}`;
|
|
23
|
+
export declare const SURROGATE_ARGS_ID: `${string}-${string}-${string}-${string}-${string}`;
|
|
24
|
+
export declare const getArgsKeyByValues: (values: readonly (string | undefined)[]) => string;
|
|
25
|
+
export declare const getArgsKey: (args: TestRunArgs) => string;
|
|
26
|
+
export declare const createTestRunLookup: <T>(entries: readonly (readonly [TestRunCoordinates, T])[]) => TestRunLookup<T>;
|
|
27
|
+
export declare const lookupTestAttempts: <T>(lookup: TestRunLookup<T>, { args, device, testPlan }: TestRunSelector) => T[] | undefined;
|
|
28
|
+
export declare const lookupTestAttempt: <Data>(lookup: TestRunLookup<Data>, selector: TestRunSelector) => Data | undefined;
|
|
29
|
+
export declare const lookupNextTestAttempt: <Data>(lookup: TestRunLookup<Data>, selector: TestRunSelector, pred: (data: Data) => boolean) => Data | undefined;
|
|
30
|
+
export declare const groupBy: <T, K>(values: readonly T[], keyFn: (v: T) => K) => Map<K, T[]>;
|
|
31
|
+
export declare const mappedGroupBy: <T, K, G>(values: readonly T[], keyFn: (v: T) => K, groupMapFn: (group: T[]) => G) => Map<K, G>;
|
|
32
|
+
export declare const getTargetDetails: ({ architecture, model, platform, osVersion }?: TargetDescriptor) => string | undefined;
|
|
33
|
+
export declare const compareChronologically: ({ start: startA, stop: stopA }: RawStep, { start: startB, stop: stopB }: RawStep) => number;
|
|
34
|
+
export declare const toSortedSteps: <T extends RawStep>(...stepArrays: readonly (readonly T[])[]) => T[];
|
|
35
|
+
export declare const secondsToMilliseconds: (seconds: Unknown<number>) => number | undefined;
|
|
36
|
+
export declare const parseAsAllureApiActivity: (title: string | undefined) => AllureApiCall | undefined;
|
|
37
|
+
export declare const splitApiCallAndValue: (text: string) => {
|
|
38
|
+
apiCall: string;
|
|
39
|
+
value: string;
|
|
40
|
+
};
|
|
41
|
+
export declare const applyApiCalls: (testResult: RawTestResult, apiCalls: readonly AllureApiCall[]) => void;
|
|
42
|
+
export declare const createTestLabels: ({ hostName, projectName, bundle, suites, className, functionName, tags, }: LabelsInputData) => RawTestLabel[];
|
|
43
|
+
export declare const getDefaultAttachmentName: (index: number, length: number) => string;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isDefined, isNumber } from "../validation.js";
|
|
3
|
+
export const MS_IN_S = 1000;
|
|
4
|
+
export const ALLURE_API_ACTIVITY_PREFIX = "allure.";
|
|
5
|
+
export const statusPriorities = new Map([
|
|
6
|
+
["failed", 0],
|
|
7
|
+
["broken", 1],
|
|
8
|
+
["unknown", 2],
|
|
9
|
+
["skipped", 3],
|
|
10
|
+
["passed", 4],
|
|
11
|
+
]);
|
|
12
|
+
export const utiToMediaType = {
|
|
13
|
+
"public.plain-text": "text/plain",
|
|
14
|
+
"public.utf8-plain-text": "text/plain",
|
|
15
|
+
"public.utf16-external-plain-text": "text/plain",
|
|
16
|
+
"public.utf16-plain-text": "text/plain",
|
|
17
|
+
"public.rtf": "text/rtf",
|
|
18
|
+
"public.html": "text/html",
|
|
19
|
+
"public.xml": "text/xml",
|
|
20
|
+
"public.source-code": "text/plain",
|
|
21
|
+
"public.c-source": "text/plain",
|
|
22
|
+
"public.objective-c-source": "text/plain",
|
|
23
|
+
"public.c-plus-plus-source": "text/plain",
|
|
24
|
+
"public.objective-c-plus-plus-source": "text/plain",
|
|
25
|
+
"public.c-header": "text/plain",
|
|
26
|
+
"public.c-plus-plus-header": "text/plain",
|
|
27
|
+
"com.sun.java-source": "text/plain",
|
|
28
|
+
"public.script": "text/plain",
|
|
29
|
+
"public.assembly-source": "text/plain",
|
|
30
|
+
"com.apple.rez-source": "text/plain",
|
|
31
|
+
"public.mig-source": "text/plain",
|
|
32
|
+
"com.apple.symbol-export": "text/plain",
|
|
33
|
+
"com.netscape.javascript-source": "text/plain",
|
|
34
|
+
"public.shell-script": "text/plain",
|
|
35
|
+
"public.csh-script": "text/plain",
|
|
36
|
+
"public.perl-script": "text/plain",
|
|
37
|
+
"public.python-script": "text/plain",
|
|
38
|
+
"public.ruby-script": "text/plain",
|
|
39
|
+
"public.php-script": "text/plain",
|
|
40
|
+
"com.sun.java-web-start": "text/plain",
|
|
41
|
+
"com.apple.applescript.text": "text/plain",
|
|
42
|
+
"com.microsoft.windows-executable": "application/x-msdownload",
|
|
43
|
+
"com.microsoft.windows-dynamic-link-library": "application/x-msdownload",
|
|
44
|
+
"com.sun.java-archive": "application/java-archive",
|
|
45
|
+
"com.apple.quartz-composer-composition": "application/x-quartzcomposer",
|
|
46
|
+
"org.gnu.gnu-tar-archive": "application/x-gtar",
|
|
47
|
+
"public.tar-archive": "application/x-tar",
|
|
48
|
+
"org.gnu.gnu-zip-archive": "application/gzip",
|
|
49
|
+
"org.gnu.gnu-zip-tar-archive": "application/gzip",
|
|
50
|
+
"com.apple.binhex-archive": "application/mac-binhex40",
|
|
51
|
+
"com.apple.macbinary-archive": "application/x-macbinary",
|
|
52
|
+
"public.heic": "image/heic",
|
|
53
|
+
"public.heif": "image/heif",
|
|
54
|
+
"public.jpeg": "image/jpeg",
|
|
55
|
+
"public.jpeg-2000": "image/jp2",
|
|
56
|
+
"public.tiff": "image/tiff",
|
|
57
|
+
"com.apple.pict": "image/x-pict",
|
|
58
|
+
"public.png": "image/png",
|
|
59
|
+
"public.xbitmap-image": "image/x-quicktime",
|
|
60
|
+
"com.apple.quicktime-image": "image/x-quicktime",
|
|
61
|
+
"com.apple.quicktime-movie": "video/quicktime",
|
|
62
|
+
"public.avi": "video/x-msvideo",
|
|
63
|
+
"public.mpeg": "video/mpeg",
|
|
64
|
+
"public.mpeg-4": "video/mp4",
|
|
65
|
+
"public.3gpp": "video/3gpp",
|
|
66
|
+
"public.3gpp2": "video/3gpp2",
|
|
67
|
+
"public.mp3": "audio/mpeg",
|
|
68
|
+
"public.mpeg-4-audio": "audio/mp4",
|
|
69
|
+
"public.ulaw-audio": "audio/basic",
|
|
70
|
+
"public.aifc-audio": "audio/x-aiff",
|
|
71
|
+
"public.aiff-audio": "audio/x-aiff",
|
|
72
|
+
"com.pkware.zip-archive": "application/zip",
|
|
73
|
+
"com.adobe.pdf": "application/pdf",
|
|
74
|
+
"com.adobe.photoshop-image": "image/vnd.adobe.photoshop",
|
|
75
|
+
"com.compuserve.gif": "image/gif",
|
|
76
|
+
"com.microsoft.bmp": "image/bmp",
|
|
77
|
+
"com.microsoft.ico": "image/vnd.microsoft.icon",
|
|
78
|
+
"com.microsoft.word.doc": "application/msword",
|
|
79
|
+
"com.microsoft.excel.xls": "application/vnd.ms-excel",
|
|
80
|
+
"com.microsoft.powerpoint.ppt": "application/vnd.ms-powerpoint",
|
|
81
|
+
"com.microsoft.waveform-audio": "audio/x-wav",
|
|
82
|
+
"com.microsoft.advanced-systems-format": "video/x-ms-asf",
|
|
83
|
+
"com.microsoft.windows-media-wm": "video/x-ms-wm",
|
|
84
|
+
"com.microsoft.windows-media-wmv": "video/x-ms-wmv",
|
|
85
|
+
"com.microsoft.windows-media-wmp": "video/x-ms-wmp",
|
|
86
|
+
"com.microsoft.windows-media-wma": "video/x-ms-wma",
|
|
87
|
+
"com.microsoft.advanced-stream-redirector": "video/x-ms-asx",
|
|
88
|
+
"com.microsoft.windows-media-wmx": "video/x-ms-wmx",
|
|
89
|
+
"com.microsoft.windows-media-wvx": "video/x-ms-wvx",
|
|
90
|
+
"com.microsoft.windows-media-wax": "video/x-ms-wax",
|
|
91
|
+
"com.truevision.tga-image": "image/x-tga",
|
|
92
|
+
"com.sgi.sgi-image": "image/x-sgi",
|
|
93
|
+
"com.kodak.flashpix.image": "image/vnd.fpx",
|
|
94
|
+
"com.real.realmedia": "application/vnd.rn-realmedia",
|
|
95
|
+
"com.real.realaudio": "audio/vnd.rn-realaudio",
|
|
96
|
+
"com.real.smil": "application/smil",
|
|
97
|
+
"com.allume.stuffit-archive": "application/x-stuffit",
|
|
98
|
+
};
|
|
99
|
+
export const getMediaTypeByUti = (uti) => (uti ? utiToMediaType[uti] : undefined);
|
|
100
|
+
export const prependTitle = (title, text, spaces) => [title, ...text.split("\n").map((l) => `${" ".repeat(spaces)}${l}`)].join("\n");
|
|
101
|
+
export const getWorstStatusWithDetails = (failureSteps) => {
|
|
102
|
+
const sortedFailureSteps = [...failureSteps];
|
|
103
|
+
sortedFailureSteps.sort(({ status: statusA }, { status: statusB }) => statusPriorities.get(statusA ?? "unknown") - statusPriorities.get(statusB ?? "unknown"));
|
|
104
|
+
if (!sortedFailureSteps.length) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
const { status, trace, message: worstStepMessage } = sortedFailureSteps[0];
|
|
108
|
+
const totalFailuresCount = failureSteps.length;
|
|
109
|
+
const expectedFailuresCount = countExpectedFailures(failureSteps);
|
|
110
|
+
const message = resolveFailureMessage(worstStepMessage, totalFailuresCount, expectedFailuresCount);
|
|
111
|
+
return { status, message, trace };
|
|
112
|
+
};
|
|
113
|
+
export const countExpectedFailures = (failureSteps) => failureSteps.reduce((a, { status }) => (status === "passed" ? a + 1 : a), 0);
|
|
114
|
+
export const resolveFailureMessage = (firstFailureMessage, failuresCount, expectedFailuresCount) => {
|
|
115
|
+
switch (failuresCount) {
|
|
116
|
+
case 0:
|
|
117
|
+
return undefined;
|
|
118
|
+
case 1:
|
|
119
|
+
return firstFailureMessage;
|
|
120
|
+
default:
|
|
121
|
+
return getAggregatedFailureMessage(firstFailureMessage, failuresCount, expectedFailuresCount);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
export const getAggregatedFailureMessage = (message, failures, expected) => {
|
|
125
|
+
const [summary, prefix] = failures === expected
|
|
126
|
+
? [`${failures} expected failures have occured`, "The first one is"]
|
|
127
|
+
: expected === 0
|
|
128
|
+
? [`${failures} failures have occured`, "The first one is"]
|
|
129
|
+
: [`${failures} failures have occured (${expected} expected)`, "The first unexpected one is"];
|
|
130
|
+
return message ? prependTitle(`${summary}. ${prefix}:`, message, 2) : summary;
|
|
131
|
+
};
|
|
132
|
+
export const DEFAULT_BUNDLE_NAME = "The test bundle name is not defined";
|
|
133
|
+
export const DEFAULT_SUITE_ID = "__unknown__";
|
|
134
|
+
export const DEFAULT_SUITE_NAME = "The test suite name is not defined";
|
|
135
|
+
export const DEFAULT_TEST_NAME = "The test name is not defined";
|
|
136
|
+
export const DEFAULT_STEP_NAME = "The test name is not defined";
|
|
137
|
+
export const DEFAULT_ATTACHMENT_NAME = "Attachment";
|
|
138
|
+
export const DEFAULT_EXPECTED_FAILURE_REASON = "Expected failure";
|
|
139
|
+
export const SURROGATE_DEVICE_ID = randomUUID();
|
|
140
|
+
export const SURROGATE_TEST_PLAN_ID = randomUUID();
|
|
141
|
+
export const SURROGATE_ARGS_ID = randomUUID();
|
|
142
|
+
export const getArgsKeyByValues = (values) => values.map(String).join(",");
|
|
143
|
+
export const getArgsKey = (args) => getArgsKeyByValues(args.map((arg) => arg?.value));
|
|
144
|
+
export const createTestRunLookup = (entries) => mappedGroupBy(entries, ([{ device }]) => device ?? SURROGATE_DEVICE_ID, (deviceRuns) => mappedGroupBy(deviceRuns, ([{ testPlan }]) => testPlan ?? SURROGATE_TEST_PLAN_ID, (configRuns) => mappedGroupBy(configRuns, ([{ args }]) => (args && args.length ? getArgsKey(args) : SURROGATE_ARGS_ID), (argRuns) => {
|
|
145
|
+
argRuns.sort(([{ attempt: attemptA }], [{ attempt: attemptB }]) => (attemptA ?? 0) - (attemptB ?? 0));
|
|
146
|
+
return argRuns.map(([, data]) => data);
|
|
147
|
+
})));
|
|
148
|
+
export const lookupTestAttempts = (lookup, { args, device, testPlan }) => lookup
|
|
149
|
+
.get(device ?? SURROGATE_DEVICE_ID)
|
|
150
|
+
?.get(testPlan ?? SURROGATE_TEST_PLAN_ID)
|
|
151
|
+
?.get(args ? getArgsKeyByValues(args) : SURROGATE_ARGS_ID);
|
|
152
|
+
export const lookupTestAttempt = (lookup, selector) => {
|
|
153
|
+
const attempts = lookupTestAttempts(lookup, selector);
|
|
154
|
+
const { attempt = 0 } = selector;
|
|
155
|
+
return attempts?.[attempt];
|
|
156
|
+
};
|
|
157
|
+
export const lookupNextTestAttempt = (lookup, selector, pred) => {
|
|
158
|
+
const attempts = lookupTestAttempts(lookup, selector);
|
|
159
|
+
return attempts?.find(pred);
|
|
160
|
+
};
|
|
161
|
+
export const groupBy = (values, keyFn) => values.reduce((m, v) => {
|
|
162
|
+
const key = keyFn(v);
|
|
163
|
+
if (!m.get(key)?.push(v)) {
|
|
164
|
+
m.set(key, [v]);
|
|
165
|
+
}
|
|
166
|
+
return m;
|
|
167
|
+
}, new Map());
|
|
168
|
+
export const mappedGroupBy = (values, keyFn, groupMapFn) => {
|
|
169
|
+
const result = new Map();
|
|
170
|
+
for (const [k, g] of groupBy(values, keyFn)) {
|
|
171
|
+
result.set(k, groupMapFn(g));
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
};
|
|
175
|
+
export const getTargetDetails = ({ architecture, model, platform, osVersion } = {}) => {
|
|
176
|
+
const osPart = platform ? (osVersion ? `${platform} ${osVersion}` : platform) : undefined;
|
|
177
|
+
return [model, architecture, osPart].filter(isDefined).join(", ") || undefined;
|
|
178
|
+
};
|
|
179
|
+
export const compareChronologically = ({ start: startA, stop: stopA }, { start: startB, stop: stopB }) => (startA ?? 0) - (startB ?? 0) || (stopA ?? 0) - (stopB ?? 0);
|
|
180
|
+
export const toSortedSteps = (...stepArrays) => {
|
|
181
|
+
const allSteps = stepArrays.reduce((result, steps) => {
|
|
182
|
+
result.push(...steps);
|
|
183
|
+
return result;
|
|
184
|
+
}, []);
|
|
185
|
+
allSteps.sort(compareChronologically);
|
|
186
|
+
return allSteps;
|
|
187
|
+
};
|
|
188
|
+
export const secondsToMilliseconds = (seconds) => isNumber(seconds) ? Math.round(MS_IN_S * seconds) : undefined;
|
|
189
|
+
export const parseAsAllureApiActivity = (title) => {
|
|
190
|
+
if (isPotentialAllureApiActivity(title)) {
|
|
191
|
+
const maybeApiCall = title.slice(ALLURE_API_ACTIVITY_PREFIX.length);
|
|
192
|
+
const { apiCall, value } = splitApiCallAndValue(maybeApiCall);
|
|
193
|
+
switch (apiCall) {
|
|
194
|
+
case "id":
|
|
195
|
+
return { type: "label", value: { name: "ALLURE_ID", value } };
|
|
196
|
+
case "name":
|
|
197
|
+
return { type: "name", value };
|
|
198
|
+
case "description":
|
|
199
|
+
return { type: "description", value };
|
|
200
|
+
case "precondition":
|
|
201
|
+
return { type: "precondition", value };
|
|
202
|
+
case "expectedResult":
|
|
203
|
+
return { type: "expectedResult", value };
|
|
204
|
+
case "flaky":
|
|
205
|
+
return { type: "flaky", value: parseBooleanApiArg(value) };
|
|
206
|
+
case "muted":
|
|
207
|
+
return { type: "muted", value: parseBooleanApiArg(value) };
|
|
208
|
+
case "known":
|
|
209
|
+
return { type: "known", value: parseBooleanApiArg(value) };
|
|
210
|
+
default:
|
|
211
|
+
return parseComplexAllureApiCall(apiCall, value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
export const splitApiCallAndValue = (text) => {
|
|
216
|
+
const apiValueSeparatorIndex = indexOfAny(text, ":", "=");
|
|
217
|
+
return apiValueSeparatorIndex === -1
|
|
218
|
+
? { apiCall: text, value: "" }
|
|
219
|
+
: { apiCall: text.slice(0, apiValueSeparatorIndex).trim(), value: text.slice(apiValueSeparatorIndex + 1) };
|
|
220
|
+
};
|
|
221
|
+
export const applyApiCalls = (testResult, apiCalls) => {
|
|
222
|
+
const groupedApiCalls = mappedGroupBy(apiCalls, (v) => v.type, (g) => g.map(({ value }) => value));
|
|
223
|
+
for (const [type, values] of groupedApiCalls) {
|
|
224
|
+
applyApiCallGroup(testResult, type, values);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const applyApiCallGroup = (testResult, type, values) => {
|
|
228
|
+
switch (type) {
|
|
229
|
+
case "name":
|
|
230
|
+
testResult.name = values.at(-1);
|
|
231
|
+
break;
|
|
232
|
+
case "flaky":
|
|
233
|
+
testResult.flaky = values.at(-1);
|
|
234
|
+
break;
|
|
235
|
+
case "muted":
|
|
236
|
+
testResult.muted = values.at(-1);
|
|
237
|
+
break;
|
|
238
|
+
case "known":
|
|
239
|
+
testResult.known = values.at(-1);
|
|
240
|
+
break;
|
|
241
|
+
case "description":
|
|
242
|
+
testResult.description = mergeMarkdownBlocks(testResult.description, ...values);
|
|
243
|
+
break;
|
|
244
|
+
case "precondition":
|
|
245
|
+
testResult.precondition = mergeMarkdownBlocks(testResult.precondition, ...values);
|
|
246
|
+
break;
|
|
247
|
+
case "expectedResult":
|
|
248
|
+
testResult.expectedResult = mergeMarkdownBlocks(testResult.expectedResult, ...values);
|
|
249
|
+
break;
|
|
250
|
+
case "label":
|
|
251
|
+
testResult.labels = [...(testResult.labels ?? []), ...values];
|
|
252
|
+
break;
|
|
253
|
+
case "link":
|
|
254
|
+
testResult.links = [...(testResult.links ?? []), ...values];
|
|
255
|
+
break;
|
|
256
|
+
case "parameter":
|
|
257
|
+
testResult.parameters = [...(testResult.parameters ?? []), ...values];
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
export const createTestLabels = ({ hostName, projectName, bundle, suites, className, functionName, tags, }) => {
|
|
262
|
+
const labels = [];
|
|
263
|
+
if (hostName) {
|
|
264
|
+
labels.push({ name: "host", value: hostName });
|
|
265
|
+
}
|
|
266
|
+
const packageName = [projectName, bundle].filter(isDefined).join(".");
|
|
267
|
+
if (packageName) {
|
|
268
|
+
labels.push({ name: "package", value: packageName });
|
|
269
|
+
}
|
|
270
|
+
if (className) {
|
|
271
|
+
labels.push({ name: "testClass", value: className });
|
|
272
|
+
}
|
|
273
|
+
if (functionName) {
|
|
274
|
+
labels.push({ name: "testMethod", value: functionName });
|
|
275
|
+
}
|
|
276
|
+
if (bundle) {
|
|
277
|
+
labels.push({ name: "parentSuite", value: bundle });
|
|
278
|
+
}
|
|
279
|
+
const [suite, ...subSuites] = suites;
|
|
280
|
+
if (suite) {
|
|
281
|
+
labels.push({ name: "suite", value: suite });
|
|
282
|
+
}
|
|
283
|
+
const subSuite = subSuites.join(" > ");
|
|
284
|
+
if (subSuite) {
|
|
285
|
+
labels.push({ name: "subSuite", value: subSuite });
|
|
286
|
+
}
|
|
287
|
+
labels.push(...tags.map((value) => ({ name: "tag", value })));
|
|
288
|
+
return labels;
|
|
289
|
+
};
|
|
290
|
+
export const getDefaultAttachmentName = (index, length) => {
|
|
291
|
+
return length > 1 ? `${DEFAULT_ATTACHMENT_NAME} ${index + 1}` : DEFAULT_ATTACHMENT_NAME;
|
|
292
|
+
};
|
|
293
|
+
const mergeMarkdownBlocks = (...blocks) => blocks.filter(isDefined).reduce((a, b) => `${a}\n\n${b}`);
|
|
294
|
+
const isPotentialAllureApiActivity = (title) => isDefined(title) && title.startsWith(ALLURE_API_ACTIVITY_PREFIX);
|
|
295
|
+
const parseComplexAllureApiCall = (apiCall, value) => {
|
|
296
|
+
const { apiFn, primaryOption, secondaryOptions } = splitApiFnAndOptions(apiCall);
|
|
297
|
+
switch (apiFn) {
|
|
298
|
+
case "label":
|
|
299
|
+
return primaryOption ? parseAllureLabelApiCall(primaryOption, value) : undefined;
|
|
300
|
+
case "link":
|
|
301
|
+
return parseAllureLinkApiCall(primaryOption, secondaryOptions, value);
|
|
302
|
+
case "issue":
|
|
303
|
+
return parseAllureLinkApiCall(primaryOption, ["issue"], value);
|
|
304
|
+
case "tms":
|
|
305
|
+
return parseAllureLinkApiCall(primaryOption, ["tms"], value);
|
|
306
|
+
case "parameter":
|
|
307
|
+
return primaryOption ? parseAllureParameterApiCall(primaryOption, secondaryOptions, value) : undefined;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const splitApiFnAndOptions = (apiCall) => {
|
|
311
|
+
const apiFnEnd = apiCall.indexOf(".");
|
|
312
|
+
if (apiFnEnd === -1) {
|
|
313
|
+
const { primaryOption: apiFn, secondaryOptions } = parseAllureApiCallOptions(apiCall);
|
|
314
|
+
return { apiFn, primaryOption: undefined, secondaryOptions };
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
apiFn: apiCall.slice(0, apiFnEnd),
|
|
318
|
+
...parseAllureApiCallOptions(apiCall.slice(apiFnEnd + 1)),
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
const parseAllureLabelApiCall = (name, value) => name ? { type: "label", value: { name, value } } : undefined;
|
|
322
|
+
const parseAllureLinkApiCall = (name, [type], url) => {
|
|
323
|
+
return { type: "link", value: { name, type, url } };
|
|
324
|
+
};
|
|
325
|
+
const parseAllureParameterApiCall = (name, options, value) => {
|
|
326
|
+
const parameter = { name, value };
|
|
327
|
+
options.forEach((option) => {
|
|
328
|
+
switch (option.toLowerCase()) {
|
|
329
|
+
case "hidden":
|
|
330
|
+
parameter.hidden = true;
|
|
331
|
+
break;
|
|
332
|
+
case "excluded":
|
|
333
|
+
parameter.excluded = true;
|
|
334
|
+
break;
|
|
335
|
+
case "masked":
|
|
336
|
+
parameter.masked = true;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
return { type: "parameter", value: parameter };
|
|
341
|
+
};
|
|
342
|
+
const parseAllureApiCallOptions = (options) => {
|
|
343
|
+
const primaryEnd = options.indexOf("[");
|
|
344
|
+
if (primaryEnd !== -1) {
|
|
345
|
+
const primaryOption = decodeURIComponentSafe(options.slice(0, primaryEnd));
|
|
346
|
+
if (options.indexOf("]") === options.length - 1) {
|
|
347
|
+
return {
|
|
348
|
+
primaryOption,
|
|
349
|
+
secondaryOptions: options
|
|
350
|
+
.slice(primaryEnd + 1, -1)
|
|
351
|
+
.split(",")
|
|
352
|
+
.map((v) => decodeURIComponentSafe(v.trim())),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return { primaryOption, secondaryOptions: [] };
|
|
356
|
+
}
|
|
357
|
+
return { primaryOption: decodeURIComponentSafe(options), secondaryOptions: [] };
|
|
358
|
+
};
|
|
359
|
+
const indexOfAny = (input, ...searchStrings) => {
|
|
360
|
+
const indices = searchStrings.map((search) => input.indexOf(search)).filter((i) => i !== -1);
|
|
361
|
+
switch (indices.length) {
|
|
362
|
+
case 0:
|
|
363
|
+
return -1;
|
|
364
|
+
case 1:
|
|
365
|
+
return indices[0];
|
|
366
|
+
default:
|
|
367
|
+
return Math.min(...indices);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const parseBooleanApiArg = (value) => !value || value.toLowerCase() === "true";
|
|
371
|
+
const decodeURIComponentSafe = (value) => {
|
|
372
|
+
try {
|
|
373
|
+
return decodeURIComponent(value);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { XcActivities, XcTestDetails, XcTests } from "./xcModel.js";
|
|
2
|
+
export declare const xcrunJson: <T>(utilityName: string, ...args: readonly string[]) => Promise<import("../../validation.js").Unknown<T>>;
|
|
3
|
+
export declare const xcrunBinary: (utilityName: string, ...args: readonly string[]) => Promise<Buffer | undefined>;
|
|
4
|
+
export declare const xcresulttool: <T>(...args: readonly string[]) => Promise<import("../../validation.js").Unknown<T>>;
|
|
5
|
+
export declare const xcresulttoolBinary: (...args: readonly string[]) => Promise<Buffer | undefined>;
|
|
6
|
+
export declare const version: () => Promise<string>;
|
|
7
|
+
export declare const getTests: (xcResultPath: string) => Promise<import("../../validation.js").Unknown<XcTests>>;
|
|
8
|
+
export declare const getTestDetails: (xcResultPath: string, testId: string) => Promise<import("../../validation.js").Unknown<XcTestDetails>>;
|
|
9
|
+
export declare const getTestActivities: (xcResultPath: string, testId: string) => Promise<import("../../validation.js").Unknown<XcActivities>>;
|
|
10
|
+
export declare const exportAttachments: (xcResultPath: string, outputPath: string) => Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import console from "node:console";
|
|
2
|
+
import { invokeCliTool, invokeJsonCliTool, invokeStdoutCliTool, invokeTextStdoutCliTool } from "../../toolRunner.js";
|
|
3
|
+
export const xcrunJson = async (utilityName, ...args) => {
|
|
4
|
+
try {
|
|
5
|
+
return await invokeJsonCliTool("xcrun", [utilityName, ...args], { timeout: 1000 });
|
|
6
|
+
}
|
|
7
|
+
catch (e) {
|
|
8
|
+
console.error(e);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export const xcrunBinary = async (utilityName, ...args) => {
|
|
12
|
+
try {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of invokeStdoutCliTool("xcrun", [utilityName, ...args], { timeout: 60000 })) {
|
|
15
|
+
chunks.push(chunk);
|
|
16
|
+
}
|
|
17
|
+
return Buffer.concat(chunks);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
console.error(e);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const xcresulttool = async (...args) => await xcrunJson("xcresulttool", ...args);
|
|
24
|
+
export const xcresulttoolBinary = async (...args) => await xcrunBinary("xcresulttool", ...args);
|
|
25
|
+
export const version = async () => {
|
|
26
|
+
const stdout = invokeTextStdoutCliTool("xcrun", ["xcresulttool", "version"], { timeout: 1000 });
|
|
27
|
+
const lines = [];
|
|
28
|
+
for await (const line of stdout) {
|
|
29
|
+
lines.push(line);
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
};
|
|
33
|
+
export const getTests = async (xcResultPath) => await xcresulttool("get", "test-results", "tests", "--path", xcResultPath);
|
|
34
|
+
export const getTestDetails = async (xcResultPath, testId) => await xcresulttool("get", "test-results", "test-details", "--test-id", testId, "--path", xcResultPath);
|
|
35
|
+
export const getTestActivities = async (xcResultPath, testId) => await xcresulttool("get", "test-results", "activities", "--test-id", testId, "--path", xcResultPath);
|
|
36
|
+
export const exportAttachments = async (xcResultPath, outputPath) => {
|
|
37
|
+
await invokeCliTool("xcrun", [
|
|
38
|
+
"xcresulttool",
|
|
39
|
+
"export",
|
|
40
|
+
"attachments",
|
|
41
|
+
"--path",
|
|
42
|
+
xcResultPath,
|
|
43
|
+
"--output-path",
|
|
44
|
+
outputPath,
|
|
45
|
+
]);
|
|
46
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ResultFile } from "@allurereport/plugin-api";
|
|
2
|
+
import type { RawTestResult } from "@allurereport/reader-api";
|
|
3
|
+
import { XcresultParser } from "./model.js";
|
|
4
|
+
export default class NewApiParser extends XcresultParser {
|
|
5
|
+
#private;
|
|
6
|
+
parse(): AsyncGenerator<ResultFile | RawTestResult, void, unknown>;
|
|
7
|
+
}
|