@allurereport/reader 3.0.0-beta.3
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 +143 -0
- package/dist/allure2/index.d.ts +2 -0
- package/dist/allure2/index.js +305 -0
- package/dist/allure2/model.d.ts +76 -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/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/junitxml/index.d.ts +2 -0
- package/dist/junitxml/index.js +100 -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 +292 -0
- package/dist/xml-utils.d.ts +8 -0
- package/dist/xml-utils.js +46 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Reader
|
|
2
|
+
|
|
3
|
+
[<img src="https://allurereport.org/public/img/allure-report.svg" height="85px" alt="Allure Report logo" align="right" />](https://allurereport.org "Allure Report")
|
|
4
|
+
|
|
5
|
+
- Learn more about Allure Report at https://allurereport.org
|
|
6
|
+
- 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report
|
|
7
|
+
- ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community
|
|
8
|
+
- 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – be in touch with the latest updates
|
|
9
|
+
- 💬 [General Discussion ](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
Set of Allure Reader implementations which allow to read different files formats as Allure Results files.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Use your favorite package manager to install the package:
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
npm add @allurereport/reader
|
|
23
|
+
yarn add @allurereport/reader
|
|
24
|
+
pnpm add @allurereport/reader
|
|
25
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { cleanBadXmlCharacters, ensureInt, ensureString, isStringAnyRecord, isStringAnyRecordArray, } from "../xml-utils.js";
|
|
3
|
+
const arrayTags = new Set([
|
|
4
|
+
"test-suite.test-cases.test-case",
|
|
5
|
+
"test-suite.test-cases.test-case.steps.step",
|
|
6
|
+
"test-suite.test-cases.test-case.attachments.attachment",
|
|
7
|
+
"test-suite.test-cases.test-case.labels",
|
|
8
|
+
]);
|
|
9
|
+
const xmlParser = new XMLParser({
|
|
10
|
+
parseTagValue: false,
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "",
|
|
13
|
+
removeNSPrefix: true,
|
|
14
|
+
allowBooleanAttributes: true,
|
|
15
|
+
isArray: (tagName, jPath) => arrayTags.has(jPath),
|
|
16
|
+
});
|
|
17
|
+
const readerId = "allure1";
|
|
18
|
+
export const allure1 = {
|
|
19
|
+
async read(visitor, data) {
|
|
20
|
+
if (data.getOriginalFileName().endsWith("-testsuite.xml")) {
|
|
21
|
+
try {
|
|
22
|
+
const asBuffer = await data.asBuffer();
|
|
23
|
+
if (!asBuffer) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const content = cleanBadXmlCharacters(asBuffer).toString("utf-8");
|
|
27
|
+
const parsed = xmlParser.parse(content);
|
|
28
|
+
return await parseRootElement(visitor, parsed);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.error("error parsing", data.getOriginalFileName(), e);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
},
|
|
37
|
+
readerId: () => readerId,
|
|
38
|
+
};
|
|
39
|
+
const parseRootElement = async (visitor, xml) => {
|
|
40
|
+
const { "test-suite": testSuite } = xml;
|
|
41
|
+
if (!isStringAnyRecord(testSuite)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return await parseTestSuite(visitor, testSuite);
|
|
45
|
+
};
|
|
46
|
+
const parseTestSuite = async (visitor, testSuite) => {
|
|
47
|
+
const { name: testSuiteName, "test-cases": testCases } = testSuite;
|
|
48
|
+
if (!isStringAnyRecord(testCases)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const { "test-case": testCase } = testCases;
|
|
52
|
+
if (!isStringAnyRecordArray(testCase)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
for (const tc of testCase) {
|
|
56
|
+
await parseTestCase(visitor, { name: ensureString(testSuiteName) }, tc);
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
const parseTestCase = async (visitor, testSuite, testCase) => {
|
|
61
|
+
const { name: nameElement, status: statusElement, failure: failureElement, parameters: parametersElement, steps: stepsElement, start: startElement, stop: stopElement, } = testCase;
|
|
62
|
+
const name = ensureString(nameElement);
|
|
63
|
+
const status = convertStatus(ensureString(statusElement));
|
|
64
|
+
const { start, stop, duration } = parseTime(startElement, stopElement);
|
|
65
|
+
const { message, trace } = parseFailure(failureElement);
|
|
66
|
+
const parameters = parseParameters(parametersElement);
|
|
67
|
+
const steps = parseSteps(stepsElement);
|
|
68
|
+
await visitor.visitTestResult({ name, status, start, stop, duration, message, trace, parameters, steps }, { readerId });
|
|
69
|
+
};
|
|
70
|
+
const parseFailure = (element) => {
|
|
71
|
+
if (!isStringAnyRecord(element)) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
const { message, "stack-trace": trace } = element;
|
|
75
|
+
return { message: ensureString(message), trace: ensureString(trace) };
|
|
76
|
+
};
|
|
77
|
+
const parseTime = (startElement, stopElement) => {
|
|
78
|
+
const start = ensureInt(startElement);
|
|
79
|
+
const stop = ensureInt(stopElement);
|
|
80
|
+
const duration = stop !== undefined && start !== undefined ? Math.max(0, stop - start) : undefined;
|
|
81
|
+
return { start, stop, duration };
|
|
82
|
+
};
|
|
83
|
+
const parseSteps = (element) => {
|
|
84
|
+
if (!isStringAnyRecord(element)) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const { step: stepElement } = element;
|
|
88
|
+
if (!isStringAnyRecordArray(stepElement)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return stepElement.map((step) => {
|
|
92
|
+
const { name, title, status, start: startElement, stop: stopElement, steps: stepsElement } = step;
|
|
93
|
+
const { start, stop, duration } = parseTime(startElement, stopElement);
|
|
94
|
+
const steps = parseSteps(stepsElement);
|
|
95
|
+
return {
|
|
96
|
+
name: ensureString(title, ensureString(name)),
|
|
97
|
+
status: convertStatus(ensureString(status)),
|
|
98
|
+
start,
|
|
99
|
+
stop,
|
|
100
|
+
duration,
|
|
101
|
+
steps,
|
|
102
|
+
type: "step",
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const parseParameters = (element) => {
|
|
107
|
+
if (!isStringAnyRecord(element)) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const { parameter } = element;
|
|
111
|
+
if (!isStringAnyRecordArray(parameter)) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return parameter
|
|
115
|
+
.filter((p) => {
|
|
116
|
+
const { kind } = p;
|
|
117
|
+
if (!kind) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
const kindString = ensureString(kind);
|
|
121
|
+
return kindString?.toLowerCase() === "argument";
|
|
122
|
+
})
|
|
123
|
+
.map((p) => {
|
|
124
|
+
const { name, value } = p;
|
|
125
|
+
return { name: ensureString(name), value: ensureString(value) };
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
const convertStatus = (status) => {
|
|
129
|
+
switch (status?.toLowerCase() ?? "unknown") {
|
|
130
|
+
case "failed":
|
|
131
|
+
return "failed";
|
|
132
|
+
case "broken":
|
|
133
|
+
return "broken";
|
|
134
|
+
case "passed":
|
|
135
|
+
return "passed";
|
|
136
|
+
case "skipped":
|
|
137
|
+
case "canceled":
|
|
138
|
+
case "pending":
|
|
139
|
+
return "skipped";
|
|
140
|
+
default:
|
|
141
|
+
return "unknown";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { notNull } from "@allurereport/core-api";
|
|
2
|
+
import { XMLParser } from "fast-xml-parser";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { parseProperties } from "../properties.js";
|
|
5
|
+
import { cleanBadXmlCharacters, ensureBoolean, ensureInt, ensureString, isStringAnyRecord, isStringAnyRecordArray, } from "../xml-utils.js";
|
|
6
|
+
import { ParameterMode } from "./model.js";
|
|
7
|
+
const arrayTags = new Set(["environment.parameter"]);
|
|
8
|
+
const xmlParser = new XMLParser({
|
|
9
|
+
parseTagValue: false,
|
|
10
|
+
ignoreAttributes: false,
|
|
11
|
+
attributeNamePrefix: "",
|
|
12
|
+
removeNSPrefix: true,
|
|
13
|
+
allowBooleanAttributes: true,
|
|
14
|
+
isArray: (tagName, jPath) => arrayTags.has(jPath),
|
|
15
|
+
});
|
|
16
|
+
const readerId = "allure2";
|
|
17
|
+
export const allure2 = {
|
|
18
|
+
async read(visitor, data) {
|
|
19
|
+
if (data.getOriginalFileName().match(/.*-attachment(\..+)?/)) {
|
|
20
|
+
await visitor.visitAttachmentFile(data, { readerId });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (data.getOriginalFileName().endsWith("-result.json")) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = await data.asJson();
|
|
26
|
+
if (parsed && isStringAnyRecord(parsed)) {
|
|
27
|
+
await processTestResult(visitor, parsed, data.getOriginalFileName());
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
console.error("error parsing", data.getOriginalFileName(), e);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (data.getOriginalFileName().endsWith("-container.json")) {
|
|
37
|
+
const parsed = await data.asJson();
|
|
38
|
+
if (parsed) {
|
|
39
|
+
await processTestResultContainer(visitor, parsed);
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (data.getOriginalFileName() === "executor.json") {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = await data.asJson();
|
|
46
|
+
if (parsed && isStringAnyRecord(parsed)) {
|
|
47
|
+
await processExecutor(visitor, parsed);
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
console.error("error parsing", data.getOriginalFileName(), e);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (data.getOriginalFileName() === "categories.json") {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = await data.asJson();
|
|
59
|
+
if (parsed && isStringAnyRecordArray(parsed)) {
|
|
60
|
+
await processCategories(visitor, parsed);
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
console.error("error parsing", data.getOriginalFileName(), e);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (data.getOriginalFileName() === "environment.properties") {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await data.asUtf8String();
|
|
72
|
+
if (raw) {
|
|
73
|
+
const parsed = parseProperties(raw);
|
|
74
|
+
if (parsed && isStringAnyRecord(parsed)) {
|
|
75
|
+
await processEnvironment(visitor, parsed);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.error("error parsing", data.getOriginalFileName(), e);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (data.getOriginalFileName() === "environment.xml") {
|
|
86
|
+
try {
|
|
87
|
+
const asBuffer = await data.asBuffer();
|
|
88
|
+
if (!asBuffer) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const content = cleanBadXmlCharacters(asBuffer).toString("utf-8");
|
|
92
|
+
const parsed = xmlParser.parse(content);
|
|
93
|
+
if (isStringAnyRecord(parsed)) {
|
|
94
|
+
await processEnvironmentXml(visitor, parsed);
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.error("error parsing", data.getOriginalFileName(), e);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
},
|
|
105
|
+
readerId: () => readerId,
|
|
106
|
+
};
|
|
107
|
+
const processTestResult = async (visitor, result, originalFileName) => {
|
|
108
|
+
const dest = {
|
|
109
|
+
uuid: ensureString(result.uuid),
|
|
110
|
+
fullName: ensureString(result.fullName),
|
|
111
|
+
name: ensureString(result.name),
|
|
112
|
+
testId: ensureString(result.testCaseId),
|
|
113
|
+
historyId: ensureString(result.historyId),
|
|
114
|
+
start: ensureInt(result.start),
|
|
115
|
+
stop: ensureInt(result.stop),
|
|
116
|
+
description: ensureString(result.description),
|
|
117
|
+
descriptionHtml: ensureString(result.descriptionHtml),
|
|
118
|
+
status: convertStatus(result.status),
|
|
119
|
+
message: ensureString(result?.statusDetails?.message),
|
|
120
|
+
trace: ensureString(result?.statusDetails?.trace),
|
|
121
|
+
flaky: ensureBoolean(result?.statusDetails?.flaky),
|
|
122
|
+
known: ensureBoolean(result?.statusDetails?.known),
|
|
123
|
+
muted: ensureBoolean(result?.statusDetails?.muted),
|
|
124
|
+
parameters: result?.parameters?.filter(notNull)?.map(convertParameter),
|
|
125
|
+
steps: [
|
|
126
|
+
...(result?.steps?.filter(notNull)?.map(convertStep) ?? []),
|
|
127
|
+
...(result?.attachments?.filter(notNull)?.map(convertAttachment) ?? []),
|
|
128
|
+
],
|
|
129
|
+
links: result?.links?.filter(notNull)?.map(convertLink),
|
|
130
|
+
labels: result.labels?.filter(notNull)?.map(convertLabel),
|
|
131
|
+
};
|
|
132
|
+
await visitor.visitTestResult(dest, { readerId, metadata: { originalFileName } });
|
|
133
|
+
};
|
|
134
|
+
const processExecutor = async (visitor, result) => {
|
|
135
|
+
await visitor.visitMetadata({
|
|
136
|
+
allure2_executor: {
|
|
137
|
+
name: ensureString(result.name),
|
|
138
|
+
type: ensureString(result.type),
|
|
139
|
+
url: ensureString(result.url),
|
|
140
|
+
buildOrder: ensureInt(result.buildOrder),
|
|
141
|
+
buildName: ensureString(result.buildName),
|
|
142
|
+
buildUrl: ensureString(result.buildUrl),
|
|
143
|
+
reportName: ensureString(result.reportName),
|
|
144
|
+
reportUrl: ensureString(result.reportUrl),
|
|
145
|
+
},
|
|
146
|
+
}, { readerId });
|
|
147
|
+
};
|
|
148
|
+
const processCategories = async (visitor, result) => {
|
|
149
|
+
const data = result.map((value) => ({
|
|
150
|
+
name: ensureString(value.name),
|
|
151
|
+
description: ensureString(value.description),
|
|
152
|
+
descriptionHtml: ensureString(value.descriptionHtml),
|
|
153
|
+
messageRegex: ensureString(value.messageRegex),
|
|
154
|
+
traceRegex: ensureString(value.traceRegex),
|
|
155
|
+
matchedStatuses: Array.isArray(value.matchedStatuses)
|
|
156
|
+
? value.matchedStatuses
|
|
157
|
+
.map((v) => ensureString(v))
|
|
158
|
+
.filter(notNull)
|
|
159
|
+
.map((v) => v)
|
|
160
|
+
: [],
|
|
161
|
+
flaky: ensureBoolean(value.flaky),
|
|
162
|
+
}));
|
|
163
|
+
await visitor.visitMetadata({
|
|
164
|
+
allure2_categories: data,
|
|
165
|
+
}, { readerId });
|
|
166
|
+
};
|
|
167
|
+
const processEnvironmentXml = async (visitor, result) => {
|
|
168
|
+
const { environment } = result;
|
|
169
|
+
if (!isStringAnyRecord(environment)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const { parameter: parameters } = environment;
|
|
173
|
+
if (!isStringAnyRecordArray(parameters)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const data = [];
|
|
177
|
+
parameters.forEach((param) => {
|
|
178
|
+
const { key, value } = param;
|
|
179
|
+
const stringKey = ensureString(key);
|
|
180
|
+
const stringValue = ensureString(value);
|
|
181
|
+
if (stringKey && stringValue) {
|
|
182
|
+
data.push({ name: stringKey, values: [stringValue] });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
await visitor.visitMetadata({
|
|
186
|
+
allure_environment: data,
|
|
187
|
+
}, {
|
|
188
|
+
readerId,
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
const processEnvironment = async (visitor, result) => {
|
|
192
|
+
const data = Object.keys(result).map((key) => {
|
|
193
|
+
const rawValue = result[key];
|
|
194
|
+
const value = typeof rawValue === "string" ? rawValue : JSON.stringify(rawValue);
|
|
195
|
+
return {
|
|
196
|
+
name: key,
|
|
197
|
+
values: [value],
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
await visitor.visitMetadata({
|
|
201
|
+
allure_environment: data,
|
|
202
|
+
}, {
|
|
203
|
+
readerId,
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
async function processFixtures(visitor, fixtures, type, children) {
|
|
207
|
+
if (fixtures) {
|
|
208
|
+
for (const fixture of fixtures) {
|
|
209
|
+
const dist = convertFixture(type, children, fixture);
|
|
210
|
+
await visitor.visitTestFixtureResult(dist, { readerId });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const processTestResultContainer = async (visitor, result) => {
|
|
215
|
+
if (result.children && result.children.length > 0) {
|
|
216
|
+
await processFixtures(visitor, result.befores, "before", result.children);
|
|
217
|
+
await processFixtures(visitor, result.afters, "after", result.children);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const convertStatus = (status) => {
|
|
221
|
+
switch ((status ?? "unknown").toLowerCase()) {
|
|
222
|
+
case "failed":
|
|
223
|
+
return "failed";
|
|
224
|
+
case "broken":
|
|
225
|
+
return "broken";
|
|
226
|
+
case "passed":
|
|
227
|
+
return "passed";
|
|
228
|
+
case "skipped":
|
|
229
|
+
return "skipped";
|
|
230
|
+
default:
|
|
231
|
+
return "unknown";
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const convertFixture = (type, children, fixture) => {
|
|
235
|
+
return {
|
|
236
|
+
uuid: randomUUID(),
|
|
237
|
+
testResults: children,
|
|
238
|
+
type,
|
|
239
|
+
name: fixture.name,
|
|
240
|
+
start: ensureInt(fixture.start),
|
|
241
|
+
stop: ensureInt(fixture.stop),
|
|
242
|
+
status: convertStatus(fixture.status),
|
|
243
|
+
message: ensureString(fixture?.statusDetails?.message),
|
|
244
|
+
trace: ensureString(fixture?.statusDetails?.trace),
|
|
245
|
+
steps: [
|
|
246
|
+
...(fixture?.steps?.filter(notNull)?.map(convertStep) ?? []),
|
|
247
|
+
...(fixture?.attachments?.filter(notNull)?.map(convertAttachment) ?? []),
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
const convertStep = (step) => {
|
|
252
|
+
const steps = step?.steps ?? [];
|
|
253
|
+
const attachments = step?.attachments ?? [];
|
|
254
|
+
if (steps.length === 0 && attachments.length === 1 && step.name === attachments[0].name) {
|
|
255
|
+
return {
|
|
256
|
+
...convertAttachment(attachments[0]),
|
|
257
|
+
start: step.start,
|
|
258
|
+
stop: step.stop,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
name: ensureString(step.name),
|
|
263
|
+
parameters: step?.parameters?.filter(notNull)?.map(convertParameter),
|
|
264
|
+
status: convertStatus(step.status),
|
|
265
|
+
message: ensureString(step?.statusDetails?.message),
|
|
266
|
+
trace: ensureString(step?.statusDetails?.trace),
|
|
267
|
+
steps: [
|
|
268
|
+
...(steps.filter(notNull)?.map(convertStep) ?? []),
|
|
269
|
+
...(attachments.filter(notNull)?.map(convertAttachment) ?? []),
|
|
270
|
+
],
|
|
271
|
+
start: ensureInt(step.start),
|
|
272
|
+
stop: ensureInt(step.stop),
|
|
273
|
+
type: "step",
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
const convertParameter = ({ mode, name, value, excluded }) => {
|
|
277
|
+
return {
|
|
278
|
+
name: ensureString(name),
|
|
279
|
+
value: ensureString(value),
|
|
280
|
+
excluded: ensureBoolean(excluded),
|
|
281
|
+
hidden: mode === ParameterMode.HIDDEN,
|
|
282
|
+
masked: mode === ParameterMode.MASKED,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
const convertAttachment = ({ name, type, source }) => {
|
|
286
|
+
return {
|
|
287
|
+
name: ensureString(name),
|
|
288
|
+
contentType: ensureString(type),
|
|
289
|
+
originalFileName: ensureString(source),
|
|
290
|
+
type: "attachment",
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
const convertLabel = ({ name, value }) => {
|
|
294
|
+
return {
|
|
295
|
+
name: ensureString(name),
|
|
296
|
+
value: ensureString(value),
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
const convertLink = ({ name, url, type }) => {
|
|
300
|
+
return {
|
|
301
|
+
name: ensureString(name),
|
|
302
|
+
url: ensureString(url),
|
|
303
|
+
type: ensureString(type),
|
|
304
|
+
};
|
|
305
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface Attachment {
|
|
2
|
+
name?: string;
|
|
3
|
+
type?: string;
|
|
4
|
+
source?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare enum Status {
|
|
7
|
+
FAILED = "failed",
|
|
8
|
+
BROKEN = "broken",
|
|
9
|
+
PASSED = "passed",
|
|
10
|
+
SKIPPED = "skipped"
|
|
11
|
+
}
|
|
12
|
+
export declare enum Stage {
|
|
13
|
+
SCHEDULED = "scheduled",
|
|
14
|
+
RUNNING = "running",
|
|
15
|
+
FINISHED = "finished",
|
|
16
|
+
PENDING = "pending",
|
|
17
|
+
INTERRUPTED = "interrupted"
|
|
18
|
+
}
|
|
19
|
+
export declare enum ParameterMode {
|
|
20
|
+
HIDDEN = "hidden",
|
|
21
|
+
MASKED = "masked",
|
|
22
|
+
DEFAULT = "default"
|
|
23
|
+
}
|
|
24
|
+
export interface Label {
|
|
25
|
+
name?: string;
|
|
26
|
+
value?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface Link {
|
|
29
|
+
name?: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
type?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Parameter {
|
|
34
|
+
name?: string;
|
|
35
|
+
value?: string;
|
|
36
|
+
excluded?: boolean;
|
|
37
|
+
mode?: ParameterMode;
|
|
38
|
+
}
|
|
39
|
+
export interface StatusDetails {
|
|
40
|
+
message?: string;
|
|
41
|
+
trace?: string;
|
|
42
|
+
known?: boolean;
|
|
43
|
+
muted?: boolean;
|
|
44
|
+
flaky?: boolean;
|
|
45
|
+
}
|
|
46
|
+
interface ExecutableItem {
|
|
47
|
+
name?: string;
|
|
48
|
+
status?: Status;
|
|
49
|
+
statusDetails?: StatusDetails;
|
|
50
|
+
stage?: Stage;
|
|
51
|
+
description?: string;
|
|
52
|
+
descriptionHtml?: string;
|
|
53
|
+
steps?: StepResult[];
|
|
54
|
+
attachments?: Attachment[];
|
|
55
|
+
parameters?: Parameter[];
|
|
56
|
+
start?: number;
|
|
57
|
+
stop?: number;
|
|
58
|
+
}
|
|
59
|
+
export type FixtureResult = ExecutableItem;
|
|
60
|
+
export type StepResult = ExecutableItem;
|
|
61
|
+
export interface TestResult extends ExecutableItem {
|
|
62
|
+
uuid?: string;
|
|
63
|
+
historyId?: string;
|
|
64
|
+
fullName?: string;
|
|
65
|
+
testCaseId?: string;
|
|
66
|
+
labels?: Label[];
|
|
67
|
+
links?: Link[];
|
|
68
|
+
}
|
|
69
|
+
export interface TestResultContainer {
|
|
70
|
+
uuid?: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
children?: string[];
|
|
73
|
+
befores?: FixtureResult[];
|
|
74
|
+
afters?: FixtureResult[];
|
|
75
|
+
}
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export var Status;
|
|
2
|
+
(function (Status) {
|
|
3
|
+
Status["FAILED"] = "failed";
|
|
4
|
+
Status["BROKEN"] = "broken";
|
|
5
|
+
Status["PASSED"] = "passed";
|
|
6
|
+
Status["SKIPPED"] = "skipped";
|
|
7
|
+
})(Status || (Status = {}));
|
|
8
|
+
export var Stage;
|
|
9
|
+
(function (Stage) {
|
|
10
|
+
Stage["SCHEDULED"] = "scheduled";
|
|
11
|
+
Stage["RUNNING"] = "running";
|
|
12
|
+
Stage["FINISHED"] = "finished";
|
|
13
|
+
Stage["PENDING"] = "pending";
|
|
14
|
+
Stage["INTERRUPTED"] = "interrupted";
|
|
15
|
+
})(Stage || (Stage = {}));
|
|
16
|
+
export var ParameterMode;
|
|
17
|
+
(function (ParameterMode) {
|
|
18
|
+
ParameterMode["HIDDEN"] = "hidden";
|
|
19
|
+
ParameterMode["MASKED"] = "masked";
|
|
20
|
+
ParameterMode["DEFAULT"] = "default";
|
|
21
|
+
})(ParameterMode || (ParameterMode = {}));
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import * as console from "node:console";
|
|
3
|
+
import { ensureString, isEmptyElement, isStringAnyRecord, isStringAnyRecordArray } from "../xml-utils.js";
|
|
4
|
+
const arrayTags = new Set(["testsuite.testcase", "testsuites.testsuite", "testsuites.testsuite.testcase"]);
|
|
5
|
+
const xmlParser = new XMLParser({
|
|
6
|
+
parseTagValue: false,
|
|
7
|
+
ignoreAttributes: false,
|
|
8
|
+
attributeNamePrefix: "",
|
|
9
|
+
removeNSPrefix: true,
|
|
10
|
+
allowBooleanAttributes: true,
|
|
11
|
+
isArray: (tagName, jPath) => arrayTags.has(jPath),
|
|
12
|
+
});
|
|
13
|
+
const readerId = "junit";
|
|
14
|
+
export const junitXml = {
|
|
15
|
+
async read(visitor, data) {
|
|
16
|
+
if (data.getOriginalFileName().endsWith(".xml")) {
|
|
17
|
+
try {
|
|
18
|
+
const content = await data.asUtf8String();
|
|
19
|
+
if (!content) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const parsed = xmlParser.parse(content);
|
|
23
|
+
return await parseRootElement(visitor, parsed);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.error("error parsing", data.getOriginalFileName(), e);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
},
|
|
32
|
+
readerId: () => readerId,
|
|
33
|
+
};
|
|
34
|
+
const parseRootElement = async (visitor, xml) => {
|
|
35
|
+
const { testsuite: testSuite } = xml;
|
|
36
|
+
if (isEmptyElement(testSuite)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (testSuite === undefined) {
|
|
40
|
+
const { testsuites: testSuites } = xml;
|
|
41
|
+
if (isEmptyElement(testSuites)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (!isStringAnyRecord(testSuites)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const { testsuite: testSuitesArray } = testSuites;
|
|
48
|
+
if (isEmptyElement(testSuitesArray)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (!isStringAnyRecordArray(testSuitesArray)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
for (const testSuitesArrayElement of testSuitesArray) {
|
|
55
|
+
await parseTestSuite(visitor, testSuitesArrayElement);
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (!isStringAnyRecord(testSuite)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
await parseTestSuite(visitor, testSuite);
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
const parseTestSuite = async (visitor, testSuite) => {
|
|
66
|
+
const { name, testcase } = testSuite;
|
|
67
|
+
if (!isStringAnyRecordArray(testcase)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const testcaseElement of testcase) {
|
|
71
|
+
await parseTestCase(visitor, { name: ensureString(name) }, testcaseElement);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const parseTestCase = async (visitor, suite, testCase) => {
|
|
75
|
+
const { name, classname, time, failure, skipped } = testCase;
|
|
76
|
+
const { status, message, trace } = getStatus(failure, skipped);
|
|
77
|
+
await visitor.visitTestResult({
|
|
78
|
+
name: ensureString(name),
|
|
79
|
+
status,
|
|
80
|
+
message,
|
|
81
|
+
trace,
|
|
82
|
+
}, { readerId });
|
|
83
|
+
};
|
|
84
|
+
const getStatus = (failure, skipped) => {
|
|
85
|
+
if (isEmptyElement(failure)) {
|
|
86
|
+
return { status: "failed" };
|
|
87
|
+
}
|
|
88
|
+
if (isStringAnyRecord(failure)) {
|
|
89
|
+
const { message, "#text": trace } = failure;
|
|
90
|
+
return { status: "failed", message: ensureString(message), trace: ensureString(trace) };
|
|
91
|
+
}
|
|
92
|
+
if (isEmptyElement(skipped)) {
|
|
93
|
+
return { status: "skipped" };
|
|
94
|
+
}
|
|
95
|
+
if (isStringAnyRecord(skipped)) {
|
|
96
|
+
const { message, "#text": trace } = skipped;
|
|
97
|
+
return { status: "skipped", message: ensureString(message), trace: ensureString(trace) };
|
|
98
|
+
}
|
|
99
|
+
return { status: "passed" };
|
|
100
|
+
};
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ExecutorInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
url: string;
|
|
5
|
+
buildOrder: number;
|
|
6
|
+
buildName: string;
|
|
7
|
+
buildUrl: string;
|
|
8
|
+
reportName: string;
|
|
9
|
+
reportUrl: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Category {
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
descriptionHtml?: string;
|
|
15
|
+
messageRegex?: string;
|
|
16
|
+
traceRegex?: string;
|
|
17
|
+
matchedStatuses: string[];
|
|
18
|
+
flaky?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type EnvironmentInfo = Record<string, string | undefined>;
|
package/dist/model.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const parseProperties: (input: string) => Record<string, string | undefined>;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
export const parseProperties = (input) => {
|
|
2
|
+
if (!input) {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
const result = {};
|
|
6
|
+
let escape = false;
|
|
7
|
+
let skipSpace = true;
|
|
8
|
+
let isCommentLine = false;
|
|
9
|
+
let newLine = true;
|
|
10
|
+
let multiLine = false;
|
|
11
|
+
let isKey = true;
|
|
12
|
+
let key = "";
|
|
13
|
+
let value = "";
|
|
14
|
+
let unicode;
|
|
15
|
+
let unicodeRemaining = 0;
|
|
16
|
+
let escapingUnicode = false;
|
|
17
|
+
let keySpace = false;
|
|
18
|
+
let sep = false;
|
|
19
|
+
const line = () => {
|
|
20
|
+
if (key || value || sep) {
|
|
21
|
+
result[key] = value;
|
|
22
|
+
key = "";
|
|
23
|
+
value = "";
|
|
24
|
+
sep = false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const decodeString = (output, char) => {
|
|
28
|
+
if (escapingUnicode && unicodeRemaining) {
|
|
29
|
+
unicode = (unicode << 4) + hex(char);
|
|
30
|
+
if (--unicodeRemaining) {
|
|
31
|
+
return output;
|
|
32
|
+
}
|
|
33
|
+
escape = false;
|
|
34
|
+
escapingUnicode = false;
|
|
35
|
+
return output + String.fromCharCode(unicode);
|
|
36
|
+
}
|
|
37
|
+
if (char === "u") {
|
|
38
|
+
unicode = 0;
|
|
39
|
+
escapingUnicode = true;
|
|
40
|
+
unicodeRemaining = 4;
|
|
41
|
+
return output;
|
|
42
|
+
}
|
|
43
|
+
escape = false;
|
|
44
|
+
switch (char) {
|
|
45
|
+
case "t":
|
|
46
|
+
return `${output}\t`;
|
|
47
|
+
case "r":
|
|
48
|
+
return `${output}\r`;
|
|
49
|
+
case "n":
|
|
50
|
+
return `${output}\n`;
|
|
51
|
+
case "f":
|
|
52
|
+
return `${output}\f`;
|
|
53
|
+
}
|
|
54
|
+
return output + char;
|
|
55
|
+
};
|
|
56
|
+
for (const char of input) {
|
|
57
|
+
if (char === "\r") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (isCommentLine) {
|
|
61
|
+
if (char === "\n") {
|
|
62
|
+
isCommentLine = false;
|
|
63
|
+
newLine = true;
|
|
64
|
+
skipSpace = true;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (skipSpace) {
|
|
69
|
+
if (isWhitespace(char)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!multiLine && char === "\n") {
|
|
73
|
+
isKey = true;
|
|
74
|
+
keySpace = false;
|
|
75
|
+
newLine = true;
|
|
76
|
+
line();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
skipSpace = false;
|
|
80
|
+
multiLine = false;
|
|
81
|
+
}
|
|
82
|
+
if (newLine) {
|
|
83
|
+
newLine = false;
|
|
84
|
+
if (isComment(char)) {
|
|
85
|
+
isCommentLine = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (char !== "\n") {
|
|
90
|
+
if (!escape && isKey && isSeparator(char)) {
|
|
91
|
+
sep = true;
|
|
92
|
+
isKey = false;
|
|
93
|
+
keySpace = false;
|
|
94
|
+
skipSpace = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === "\\") {
|
|
98
|
+
if (escape) {
|
|
99
|
+
if (escapingUnicode) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (keySpace) {
|
|
103
|
+
keySpace = false;
|
|
104
|
+
isKey = false;
|
|
105
|
+
}
|
|
106
|
+
if (isKey) {
|
|
107
|
+
key += "\\";
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
value += "\\";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
escape = !escape;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
if (keySpace) {
|
|
117
|
+
keySpace = false;
|
|
118
|
+
isKey = false;
|
|
119
|
+
}
|
|
120
|
+
if (isKey) {
|
|
121
|
+
if (escape) {
|
|
122
|
+
key = decodeString(key, char);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (isWhitespace(char)) {
|
|
126
|
+
keySpace = true;
|
|
127
|
+
skipSpace = true;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
key += char;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
if (escape) {
|
|
135
|
+
value = decodeString(value, char);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
value += char;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
if (escape) {
|
|
145
|
+
if (!escapingUnicode) {
|
|
146
|
+
escape = false;
|
|
147
|
+
}
|
|
148
|
+
skipSpace = true;
|
|
149
|
+
multiLine = true;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
newLine = true;
|
|
153
|
+
skipSpace = true;
|
|
154
|
+
isKey = true;
|
|
155
|
+
line();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
line();
|
|
160
|
+
return result;
|
|
161
|
+
};
|
|
162
|
+
const hex = (char) => {
|
|
163
|
+
switch (char) {
|
|
164
|
+
case "0":
|
|
165
|
+
return 0;
|
|
166
|
+
case "1":
|
|
167
|
+
return 1;
|
|
168
|
+
case "2":
|
|
169
|
+
return 2;
|
|
170
|
+
case "3":
|
|
171
|
+
return 3;
|
|
172
|
+
case "4":
|
|
173
|
+
return 4;
|
|
174
|
+
case "5":
|
|
175
|
+
return 5;
|
|
176
|
+
case "6":
|
|
177
|
+
return 6;
|
|
178
|
+
case "7":
|
|
179
|
+
return 7;
|
|
180
|
+
case "8":
|
|
181
|
+
return 8;
|
|
182
|
+
case "9":
|
|
183
|
+
return 9;
|
|
184
|
+
case "a":
|
|
185
|
+
case "A":
|
|
186
|
+
return 10;
|
|
187
|
+
case "b":
|
|
188
|
+
case "B":
|
|
189
|
+
return 11;
|
|
190
|
+
case "c":
|
|
191
|
+
case "C":
|
|
192
|
+
return 12;
|
|
193
|
+
case "d":
|
|
194
|
+
case "D":
|
|
195
|
+
return 13;
|
|
196
|
+
case "e":
|
|
197
|
+
case "E":
|
|
198
|
+
return 14;
|
|
199
|
+
case "f":
|
|
200
|
+
case "F":
|
|
201
|
+
return 15;
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`Non-hex char ${char}`);
|
|
204
|
+
};
|
|
205
|
+
const escapeKey = (key) => {
|
|
206
|
+
return mapcatChars(key, (char, code) => {
|
|
207
|
+
if (isSeparator(char)) {
|
|
208
|
+
return `\\${char}`;
|
|
209
|
+
}
|
|
210
|
+
return escapeCharacter(char, code, true);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
const escapeValue = (value) => {
|
|
214
|
+
let escapeWhitespace = true;
|
|
215
|
+
return mapcatChars(value, (char, code) => {
|
|
216
|
+
if (!isWhitespace(char)) {
|
|
217
|
+
escapeWhitespace = false;
|
|
218
|
+
}
|
|
219
|
+
return escapeCharacter(char, code, escapeWhitespace);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
const mapcatChars = (value, fn) => {
|
|
223
|
+
let result = "";
|
|
224
|
+
for (let i = 0; i < value.length; i++) {
|
|
225
|
+
const char = value[i];
|
|
226
|
+
const code = value.charCodeAt(i);
|
|
227
|
+
result += fn(char, code);
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
};
|
|
231
|
+
const escapeCharacter = (char, code, escapeWhitespace) => {
|
|
232
|
+
if (isAsciiPrintable(code)) {
|
|
233
|
+
if (char === " " && escapeWhitespace) {
|
|
234
|
+
return "\\ ";
|
|
235
|
+
}
|
|
236
|
+
else if (char === "\\") {
|
|
237
|
+
return "\\\\";
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
return char;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (char === "\t") {
|
|
244
|
+
return "\\t";
|
|
245
|
+
}
|
|
246
|
+
else if (char === "\n") {
|
|
247
|
+
return "\\n";
|
|
248
|
+
}
|
|
249
|
+
else if (char === "\f") {
|
|
250
|
+
return "\\f";
|
|
251
|
+
}
|
|
252
|
+
else if (char === "\r") {
|
|
253
|
+
return "\\r";
|
|
254
|
+
}
|
|
255
|
+
else if (code < 160 || code >= 256) {
|
|
256
|
+
return escapeUnicode(code);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
return char;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const isWhitespace = (char) => {
|
|
263
|
+
switch (char) {
|
|
264
|
+
case "\t":
|
|
265
|
+
case "\f":
|
|
266
|
+
case " ":
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
};
|
|
271
|
+
const isAsciiPrintable = (code) => code > 31 && code < 127;
|
|
272
|
+
const escapeUnicode = (code) => {
|
|
273
|
+
const unicode = code.toString(16);
|
|
274
|
+
const prefix = "0".repeat(4 - unicode.length);
|
|
275
|
+
return `\\u${prefix}${unicode}`;
|
|
276
|
+
};
|
|
277
|
+
const isSeparator = (char) => {
|
|
278
|
+
switch (char) {
|
|
279
|
+
case "=":
|
|
280
|
+
case ":":
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
};
|
|
285
|
+
const isComment = (char) => {
|
|
286
|
+
switch (char) {
|
|
287
|
+
case "#":
|
|
288
|
+
case "!":
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const isEmptyElement: (obj: unknown) => obj is "";
|
|
2
|
+
export declare const ensureBoolean: (obj: unknown, fallback?: boolean) => boolean | undefined;
|
|
3
|
+
export declare const ensureString: (obj: unknown, fallback?: string) => string | undefined;
|
|
4
|
+
export declare const ensureInt: (obj: unknown) => number | undefined;
|
|
5
|
+
export declare const isStringAnyRecord: (obj: unknown) => obj is Record<string, any>;
|
|
6
|
+
export declare const isStringAnyRecordArray: (obj: unknown) => obj is Record<string, any>[];
|
|
7
|
+
export declare const isBadXmlCharacter: (c: number) => boolean;
|
|
8
|
+
export declare const cleanBadXmlCharacters: (input: Buffer) => Buffer;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const isEmptyElement = (obj) => {
|
|
2
|
+
return obj === "";
|
|
3
|
+
};
|
|
4
|
+
export const ensureBoolean = (obj, fallback) => {
|
|
5
|
+
return typeof obj === "boolean" ? obj : fallback;
|
|
6
|
+
};
|
|
7
|
+
export const ensureString = (obj, fallback) => {
|
|
8
|
+
return typeof obj === "string" ? obj : fallback;
|
|
9
|
+
};
|
|
10
|
+
export const ensureInt = (obj) => {
|
|
11
|
+
if (typeof obj === "number") {
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
const stringValue = ensureString(obj);
|
|
15
|
+
if (!stringValue) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const parsed = parseInt(stringValue);
|
|
19
|
+
return isNaN(parsed) ? undefined : parsed;
|
|
20
|
+
};
|
|
21
|
+
export const isStringAnyRecord = (obj) => {
|
|
22
|
+
if (typeof obj !== "object") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(obj)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return Object.getOwnPropertySymbols(obj).length <= 0;
|
|
29
|
+
};
|
|
30
|
+
export const isStringAnyRecordArray = (obj) => {
|
|
31
|
+
return Array.isArray(obj) && obj.every((item) => isStringAnyRecord(item));
|
|
32
|
+
};
|
|
33
|
+
export const isBadXmlCharacter = (c) => {
|
|
34
|
+
let cDataCharacter = c < 32 && c != 9 && c != 13 && c != 10;
|
|
35
|
+
cDataCharacter || (cDataCharacter = c >= 55296 && c < 57344);
|
|
36
|
+
cDataCharacter || (cDataCharacter = c == 65534 || c == 65535);
|
|
37
|
+
return cDataCharacter;
|
|
38
|
+
};
|
|
39
|
+
export const cleanBadXmlCharacters = (input) => {
|
|
40
|
+
for (let i = 0; i < input.length; i++) {
|
|
41
|
+
if (isBadXmlCharacter(input[i])) {
|
|
42
|
+
input[i] = 32;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return input;
|
|
46
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@allurereport/reader",
|
|
3
|
+
"version": "3.0.0-beta.3",
|
|
4
|
+
"description": "Collection of utilities which helps to process different kind of test results as Allure Results",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"allure",
|
|
7
|
+
"testing"
|
|
8
|
+
],
|
|
9
|
+
"repository": "https://github.com/allure-framework/allure3",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"author": "Qameta Software",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "run clean && tsc --project ./tsconfig.json",
|
|
23
|
+
"clean": "rimraf ./dist",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@allurereport/core-api": "3.0.0-beta.3",
|
|
28
|
+
"@allurereport/plugin-api": "3.0.0-beta.3",
|
|
29
|
+
"@allurereport/reader-api": "3.0.0-beta.3",
|
|
30
|
+
"fast-xml-parser": "^4.5.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@stylistic/eslint-plugin": "^2.6.1",
|
|
34
|
+
"@types/eslint": "^8.56.11",
|
|
35
|
+
"@types/node": "^20.17.9",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
37
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
38
|
+
"@vitest/runner": "^2.1.8",
|
|
39
|
+
"allure-js-commons": "^3.0.7",
|
|
40
|
+
"allure-vitest": "^3.0.7",
|
|
41
|
+
"eslint": "^8.57.0",
|
|
42
|
+
"eslint-config-prettier": "^9.1.0",
|
|
43
|
+
"eslint-plugin-import": "^2.29.1",
|
|
44
|
+
"eslint-plugin-jsdoc": "^50.0.0",
|
|
45
|
+
"eslint-plugin-n": "^17.10.1",
|
|
46
|
+
"eslint-plugin-no-null": "^1.0.2",
|
|
47
|
+
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
48
|
+
"rimraf": "^6.0.1",
|
|
49
|
+
"typescript": "^5.6.3",
|
|
50
|
+
"vitest": "^2.1.8"
|
|
51
|
+
}
|
|
52
|
+
}
|