@allurereport/reader 3.19.0 → 3.20.0
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/dist/cucumbermessages/index.d.ts +2 -0
- package/dist/cucumbermessages/index.js +210 -0
- package/dist/cucumbermessages/model.d.ts +134 -0
- package/dist/cucumbermessages/model.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/junitxml/index.js +53 -11
- package/package.json +4 -4
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { BufferResultFile, } from "@allurereport/reader-api";
|
|
3
|
+
import { isNonNullObject } from "../utils.js";
|
|
4
|
+
const readerId = "cucumber-messages";
|
|
5
|
+
const statusMap = new Map([
|
|
6
|
+
["PASSED", "passed"],
|
|
7
|
+
["FAILED", "failed"],
|
|
8
|
+
["SKIPPED", "skipped"],
|
|
9
|
+
["PENDING", "skipped"],
|
|
10
|
+
["UNDEFINED", "broken"],
|
|
11
|
+
["AMBIGUOUS", "broken"],
|
|
12
|
+
]);
|
|
13
|
+
const statusOrder = ["failed", "broken", "unknown", "skipped", "passed"];
|
|
14
|
+
const stepKey = (executionId, stepId) => JSON.stringify([executionId, stepId]);
|
|
15
|
+
export const cucumberMessages = {
|
|
16
|
+
readerId: () => readerId,
|
|
17
|
+
matches: (data) => /\.(jsonl|ndjson)$/.test(data.getOriginalFileName()),
|
|
18
|
+
read: async (visitor, data) => {
|
|
19
|
+
const originalFileName = data.getOriginalFileName();
|
|
20
|
+
try {
|
|
21
|
+
const content = await data.asUtf8String();
|
|
22
|
+
if (!content?.trim())
|
|
23
|
+
return false;
|
|
24
|
+
const messages = content
|
|
25
|
+
.split(/\r?\n/)
|
|
26
|
+
.filter((line) => line.trim())
|
|
27
|
+
.map((line) => JSON.parse(line));
|
|
28
|
+
if (!messages.every(isNonNullObject))
|
|
29
|
+
return false;
|
|
30
|
+
return await processMessages(visitor, originalFileName, messages);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error("error parsing", originalFileName, error);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const processMessages = async (visitor, originalFileName, messages) => {
|
|
39
|
+
const pickles = new Map();
|
|
40
|
+
const cases = new Map();
|
|
41
|
+
const hooks = new Map();
|
|
42
|
+
const starts = new Map();
|
|
43
|
+
const finishes = new Map();
|
|
44
|
+
const stepStarts = new Map();
|
|
45
|
+
const stepFinishes = new Map();
|
|
46
|
+
const attachments = new Map();
|
|
47
|
+
for (const message of messages) {
|
|
48
|
+
if (message.pickle)
|
|
49
|
+
pickles.set(message.pickle.id, message.pickle);
|
|
50
|
+
if (message.testCase)
|
|
51
|
+
cases.set(message.testCase.id, message.testCase);
|
|
52
|
+
if (message.hook)
|
|
53
|
+
hooks.set(message.hook.id, message.hook);
|
|
54
|
+
if (message.testCaseStarted)
|
|
55
|
+
starts.set(message.testCaseStarted.id, message.testCaseStarted);
|
|
56
|
+
if (message.testCaseFinished)
|
|
57
|
+
finishes.set(message.testCaseFinished.testCaseStartedId, message.testCaseFinished);
|
|
58
|
+
if (message.testStepStarted) {
|
|
59
|
+
const event = message.testStepStarted;
|
|
60
|
+
stepStarts.set(stepKey(event.testCaseStartedId, event.testStepId), event);
|
|
61
|
+
}
|
|
62
|
+
if (message.testStepFinished) {
|
|
63
|
+
const event = message.testStepFinished;
|
|
64
|
+
stepFinishes.set(stepKey(event.testCaseStartedId, event.testStepId), event);
|
|
65
|
+
}
|
|
66
|
+
const attachment = message.attachment;
|
|
67
|
+
if (attachment?.testCaseStartedId &&
|
|
68
|
+
typeof attachment.body === "string" &&
|
|
69
|
+
(attachment.contentEncoding === "IDENTITY" || attachment.contentEncoding === "BASE64")) {
|
|
70
|
+
const group = attachments.get(attachment.testCaseStartedId) ?? [];
|
|
71
|
+
group.push(attachment);
|
|
72
|
+
attachments.set(attachment.testCaseStartedId, group);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const gherkin = indexGherkin(messages);
|
|
76
|
+
const context = { readerId, metadata: { originalFileName } };
|
|
77
|
+
for (const execution of starts.values()) {
|
|
78
|
+
const testCase = cases.get(execution.testCaseId);
|
|
79
|
+
const pickle = testCase && pickles.get(testCase.pickleId);
|
|
80
|
+
if (!testCase || !pickle)
|
|
81
|
+
throw new Error(`Missing test case or pickle for execution ${execution.id}`);
|
|
82
|
+
const { scenario, feature } = pickle.astNodeIds.map((id) => gherkin.scenarios.get(id)).find(Boolean) ?? {};
|
|
83
|
+
const example = pickle.astNodeIds.map((id) => gherkin.examples.get(id)).find(Boolean);
|
|
84
|
+
const line = pickle.location?.line ?? example?.line ?? scenario?.location.line;
|
|
85
|
+
const name = pickle.name || "The scenario's name is not defined";
|
|
86
|
+
const finish = finishes.get(execution.id);
|
|
87
|
+
const result = {
|
|
88
|
+
uuid: randomUUID(),
|
|
89
|
+
name,
|
|
90
|
+
fullName: `${pickle.uri}${line === undefined ? "" : `:${line}`}#${name}`,
|
|
91
|
+
testCaseName: scenario?.name,
|
|
92
|
+
description: scenario?.description,
|
|
93
|
+
parameters: example?.parameters,
|
|
94
|
+
labels: [
|
|
95
|
+
...(feature ? [{ name: "feature", value: feature }] : []),
|
|
96
|
+
...pickle.tags.map(({ name: value }) => ({ name: "tag", value })),
|
|
97
|
+
],
|
|
98
|
+
start: milliseconds(execution.timestamp),
|
|
99
|
+
stop: milliseconds(finish?.timestamp),
|
|
100
|
+
steps: [],
|
|
101
|
+
};
|
|
102
|
+
const pickleSteps = new Map(pickle.steps.map((step) => [step.id, step]));
|
|
103
|
+
const executionAttachments = attachments.get(execution.id) ?? [];
|
|
104
|
+
const allSteps = [];
|
|
105
|
+
const fixtures = [];
|
|
106
|
+
for (const testStep of testCase.testSteps) {
|
|
107
|
+
const key = stepKey(execution.id, testStep.id);
|
|
108
|
+
const pickleStep = testStep.pickleStepId ? pickleSteps.get(testStep.pickleStepId) : undefined;
|
|
109
|
+
const hook = testStep.hookId ? hooks.get(testStep.hookId) : undefined;
|
|
110
|
+
const keyword = pickleStep?.astNodeIds.map((id) => gherkin.keywords.get(id)).find((value) => value !== undefined);
|
|
111
|
+
const step = {
|
|
112
|
+
...convertStep(stepStarts.get(key), stepFinishes.get(key)),
|
|
113
|
+
name: pickleStep ? `${keyword ?? ""}${pickleStep.text}` : (hook?.name ?? "Hook"),
|
|
114
|
+
steps: await readAttachments(visitor, executionAttachments.filter((attachment) => attachment.testStepId === testStep.id), pickleStep),
|
|
115
|
+
};
|
|
116
|
+
allSteps.push(step);
|
|
117
|
+
const fixtureType = hook?.type === "BEFORE_TEST_CASE" ? "before" : hook?.type === "AFTER_TEST_CASE" ? "after" : undefined;
|
|
118
|
+
if (fixtureType) {
|
|
119
|
+
fixtures.push({ ...step, type: fixtureType, testResults: [result.uuid] });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
result.steps.push(step);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
result.steps.push(...(await readAttachments(visitor, executionAttachments.filter((attachment) => !attachment.testStepId))));
|
|
126
|
+
const worst = allSteps.sort((a, b) => statusOrder.indexOf(a.status) - statusOrder.indexOf(b.status))[0];
|
|
127
|
+
result.status = finish ? (worst?.status ?? "unknown") : "unknown";
|
|
128
|
+
result.message = worst?.message;
|
|
129
|
+
result.trace = worst?.trace;
|
|
130
|
+
await visitor.visitTestResult(result, context);
|
|
131
|
+
for (const fixture of fixtures)
|
|
132
|
+
await visitor.visitTestFixtureResult(fixture, context);
|
|
133
|
+
}
|
|
134
|
+
return starts.size > 0;
|
|
135
|
+
};
|
|
136
|
+
const indexGherkin = (messages) => {
|
|
137
|
+
const scenarios = new Map();
|
|
138
|
+
const examples = new Map();
|
|
139
|
+
const keywords = new Map();
|
|
140
|
+
const visit = (children, feature) => {
|
|
141
|
+
for (const child of children) {
|
|
142
|
+
if (child.rule)
|
|
143
|
+
visit(child.rule.children, feature);
|
|
144
|
+
for (const step of child.scenario?.steps ?? child.background?.steps ?? [])
|
|
145
|
+
keywords.set(step.id, step.keyword);
|
|
146
|
+
if (!child.scenario)
|
|
147
|
+
continue;
|
|
148
|
+
const scenario = child.scenario;
|
|
149
|
+
scenarios.set(scenario.id, { scenario, feature });
|
|
150
|
+
for (const example of scenario.examples) {
|
|
151
|
+
for (const row of example.tableBody) {
|
|
152
|
+
examples.set(row.id, {
|
|
153
|
+
line: row.location.line,
|
|
154
|
+
parameters: example.tableHeader?.cells.map((cell, index) => ({ name: cell.value, value: row.cells[index]?.value })) ??
|
|
155
|
+
[],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
for (const message of messages) {
|
|
162
|
+
const feature = message.gherkinDocument?.feature;
|
|
163
|
+
if (feature)
|
|
164
|
+
visit(feature.children, feature.name);
|
|
165
|
+
}
|
|
166
|
+
return { scenarios, examples, keywords };
|
|
167
|
+
};
|
|
168
|
+
const milliseconds = (timestamp) => {
|
|
169
|
+
if (!timestamp)
|
|
170
|
+
return undefined;
|
|
171
|
+
const value = Number(timestamp.seconds) * 1000 + timestamp.nanos / 1000000;
|
|
172
|
+
return Number.isFinite(value) ? value : undefined;
|
|
173
|
+
};
|
|
174
|
+
const convertStep = (started, finished) => {
|
|
175
|
+
const result = finished?.testStepResult;
|
|
176
|
+
const start = milliseconds(started?.timestamp);
|
|
177
|
+
const duration = milliseconds(result?.duration);
|
|
178
|
+
return {
|
|
179
|
+
type: "step",
|
|
180
|
+
start,
|
|
181
|
+
stop: start !== undefined && duration !== undefined ? undefined : milliseconds(finished?.timestamp),
|
|
182
|
+
duration,
|
|
183
|
+
status: statusMap.get(result?.status ?? "UNKNOWN") ?? "unknown",
|
|
184
|
+
message: result?.exception?.message ?? result?.message,
|
|
185
|
+
trace: result?.exception?.stackTrace ?? result?.message,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
const readAttachments = async (visitor, attachments, step) => {
|
|
189
|
+
const result = [];
|
|
190
|
+
const docString = step?.argument?.docString;
|
|
191
|
+
if (docString) {
|
|
192
|
+
result.push(await visitAttachment(visitor, "Description", Buffer.from(docString.content), docString.mediaType ?? "text/plain"));
|
|
193
|
+
}
|
|
194
|
+
const table = step?.argument?.dataTable;
|
|
195
|
+
if (table) {
|
|
196
|
+
const csv = table.rows
|
|
197
|
+
.map(({ cells }) => cells.map(({ value }) => `"${value.replaceAll('"', '""')}"`).join(","))
|
|
198
|
+
.join("\n");
|
|
199
|
+
result.push(await visitAttachment(visitor, "Data", Buffer.from(csv), "text/csv"));
|
|
200
|
+
}
|
|
201
|
+
for (const attachment of attachments) {
|
|
202
|
+
result.push(await visitAttachment(visitor, attachment.fileName ?? "Attachment", Buffer.from(attachment.body, attachment.contentEncoding === "BASE64" ? "base64" : "utf8"), attachment.mediaType));
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
};
|
|
206
|
+
const visitAttachment = async (visitor, name, content, contentType) => {
|
|
207
|
+
const file = new BufferResultFile(content, randomUUID());
|
|
208
|
+
await visitor.visitAttachmentFile(file, { readerId });
|
|
209
|
+
return { type: "attachment", name, contentType, originalFileName: file.getOriginalFileName() };
|
|
210
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type CucumberTimestamp = {
|
|
2
|
+
seconds: number | string;
|
|
3
|
+
nanos: number;
|
|
4
|
+
};
|
|
5
|
+
export type CucumberLocation = {
|
|
6
|
+
line: number;
|
|
7
|
+
};
|
|
8
|
+
export type CucumberTableRow = {
|
|
9
|
+
cells: {
|
|
10
|
+
value: string;
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
export type CucumberPickleStep = {
|
|
14
|
+
id: string;
|
|
15
|
+
text: string;
|
|
16
|
+
astNodeIds: string[];
|
|
17
|
+
argument?: {
|
|
18
|
+
docString?: {
|
|
19
|
+
content: string;
|
|
20
|
+
mediaType?: string;
|
|
21
|
+
};
|
|
22
|
+
dataTable?: {
|
|
23
|
+
rows: CucumberTableRow[];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export type CucumberPickle = {
|
|
28
|
+
id: string;
|
|
29
|
+
uri: string;
|
|
30
|
+
name: string;
|
|
31
|
+
language: string;
|
|
32
|
+
location?: CucumberLocation;
|
|
33
|
+
astNodeIds: string[];
|
|
34
|
+
tags: {
|
|
35
|
+
name: string;
|
|
36
|
+
}[];
|
|
37
|
+
steps: CucumberPickleStep[];
|
|
38
|
+
};
|
|
39
|
+
export type CucumberGherkinStep = {
|
|
40
|
+
id: string;
|
|
41
|
+
keyword: string;
|
|
42
|
+
};
|
|
43
|
+
export type CucumberScenario = {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
location: CucumberLocation;
|
|
48
|
+
steps: CucumberGherkinStep[];
|
|
49
|
+
examples: {
|
|
50
|
+
tableHeader?: CucumberTableRow;
|
|
51
|
+
tableBody: (CucumberTableRow & {
|
|
52
|
+
id: string;
|
|
53
|
+
location: CucumberLocation;
|
|
54
|
+
})[];
|
|
55
|
+
}[];
|
|
56
|
+
};
|
|
57
|
+
export type CucumberGherkinChild = {
|
|
58
|
+
scenario?: CucumberScenario;
|
|
59
|
+
background?: {
|
|
60
|
+
steps: CucumberGherkinStep[];
|
|
61
|
+
};
|
|
62
|
+
rule?: {
|
|
63
|
+
children: CucumberGherkinChild[];
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export type CucumberGherkinDocument = {
|
|
67
|
+
uri?: string;
|
|
68
|
+
feature?: {
|
|
69
|
+
name: string;
|
|
70
|
+
children: CucumberGherkinChild[];
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export type CucumberHook = {
|
|
74
|
+
id: string;
|
|
75
|
+
name?: string;
|
|
76
|
+
type?: string;
|
|
77
|
+
};
|
|
78
|
+
export type CucumberTestCase = {
|
|
79
|
+
id: string;
|
|
80
|
+
pickleId: string;
|
|
81
|
+
testSteps: {
|
|
82
|
+
id: string;
|
|
83
|
+
pickleStepId?: string;
|
|
84
|
+
hookId?: string;
|
|
85
|
+
}[];
|
|
86
|
+
};
|
|
87
|
+
export type CucumberTestCaseStarted = {
|
|
88
|
+
id: string;
|
|
89
|
+
testCaseId: string;
|
|
90
|
+
attempt: number;
|
|
91
|
+
timestamp: CucumberTimestamp;
|
|
92
|
+
};
|
|
93
|
+
export type CucumberTestCaseFinished = {
|
|
94
|
+
testCaseStartedId: string;
|
|
95
|
+
timestamp: CucumberTimestamp;
|
|
96
|
+
willBeRetried: boolean;
|
|
97
|
+
};
|
|
98
|
+
export type CucumberTestStepEvent = {
|
|
99
|
+
testCaseStartedId: string;
|
|
100
|
+
testStepId: string;
|
|
101
|
+
timestamp: CucumberTimestamp;
|
|
102
|
+
};
|
|
103
|
+
export type CucumberTestStepResult = {
|
|
104
|
+
status: string;
|
|
105
|
+
duration?: CucumberTimestamp;
|
|
106
|
+
message?: string;
|
|
107
|
+
exception?: {
|
|
108
|
+
type: string;
|
|
109
|
+
message?: string;
|
|
110
|
+
stackTrace?: string;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
export type CucumberTestStepFinished = CucumberTestStepEvent & {
|
|
114
|
+
testStepResult: CucumberTestStepResult;
|
|
115
|
+
};
|
|
116
|
+
export type CucumberAttachment = {
|
|
117
|
+
body: string;
|
|
118
|
+
contentEncoding: "IDENTITY" | "BASE64";
|
|
119
|
+
mediaType: string;
|
|
120
|
+
fileName?: string;
|
|
121
|
+
testCaseStartedId?: string;
|
|
122
|
+
testStepId?: string;
|
|
123
|
+
};
|
|
124
|
+
export type CucumberMessage = {
|
|
125
|
+
gherkinDocument?: CucumberGherkinDocument;
|
|
126
|
+
pickle?: CucumberPickle;
|
|
127
|
+
hook?: CucumberHook;
|
|
128
|
+
testCase?: CucumberTestCase;
|
|
129
|
+
testCaseStarted?: CucumberTestCaseStarted;
|
|
130
|
+
testCaseFinished?: CucumberTestCaseFinished;
|
|
131
|
+
testStepStarted?: CucumberTestStepEvent;
|
|
132
|
+
testStepFinished?: CucumberTestStepFinished;
|
|
133
|
+
attachment?: CucumberAttachment;
|
|
134
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./allure1/index.js";
|
|
2
2
|
export * from "./allure2/index.js";
|
|
3
3
|
export * from "./cucumberjson/index.js";
|
|
4
|
+
export * from "./cucumbermessages/index.js";
|
|
4
5
|
export * from "./junitxml/index.js";
|
|
5
6
|
export * from "./xcresult/index.js";
|
|
6
7
|
export * from "./attachments/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./allure1/index.js";
|
|
2
2
|
export * from "./allure2/index.js";
|
|
3
3
|
export * from "./cucumberjson/index.js";
|
|
4
|
+
export * from "./cucumbermessages/index.js";
|
|
4
5
|
export * from "./junitxml/index.js";
|
|
5
6
|
export * from "./xcresult/index.js";
|
|
6
7
|
export * from "./attachments/index.js";
|
package/dist/junitxml/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import console from "node:console";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { BufferResultFile, PathResultFile } from "@allurereport/reader-api";
|
|
4
7
|
import { XMLParser } from "fast-xml-parser";
|
|
5
8
|
import { ensureString } from "../utils.js";
|
|
6
9
|
import { isEmptyElement, isStringAnyRecord, isStringAnyRecordArray } from "../xml-utils.js";
|
|
@@ -37,7 +40,8 @@ export const junitXml = {
|
|
|
37
40
|
if (!isStringAnyRecord(parsed)) {
|
|
38
41
|
return false;
|
|
39
42
|
}
|
|
40
|
-
|
|
43
|
+
const reportDir = dirname(data instanceof PathResultFile ? data.path : originalFileName);
|
|
44
|
+
return await parseRootElement(visitor, parsed, reportDir);
|
|
41
45
|
}
|
|
42
46
|
catch (e) {
|
|
43
47
|
console.error("error parsing", originalFileName, e);
|
|
@@ -46,7 +50,7 @@ export const junitXml = {
|
|
|
46
50
|
},
|
|
47
51
|
readerId: () => readerId,
|
|
48
52
|
};
|
|
49
|
-
const parseRootElement = async (visitor, xml) => {
|
|
53
|
+
const parseRootElement = async (visitor, xml, reportDir) => {
|
|
50
54
|
const { testsuite: testSuite } = xml;
|
|
51
55
|
if (isEmptyElement(testSuite)) {
|
|
52
56
|
return true;
|
|
@@ -67,26 +71,26 @@ const parseRootElement = async (visitor, xml) => {
|
|
|
67
71
|
return false;
|
|
68
72
|
}
|
|
69
73
|
for (const testSuitesArrayElement of testSuitesArray) {
|
|
70
|
-
await parseTestSuite(visitor, testSuitesArrayElement, true);
|
|
74
|
+
await parseTestSuite(visitor, testSuitesArrayElement, true, reportDir);
|
|
71
75
|
}
|
|
72
76
|
return true;
|
|
73
77
|
}
|
|
74
78
|
if (!isStringAnyRecord(testSuite)) {
|
|
75
79
|
return false;
|
|
76
80
|
}
|
|
77
|
-
await parseTestSuite(visitor, testSuite, false);
|
|
81
|
+
await parseTestSuite(visitor, testSuite, false, reportDir);
|
|
78
82
|
return true;
|
|
79
83
|
};
|
|
80
|
-
const parseTestSuite = async (visitor, testSuite, isAggregated) => {
|
|
84
|
+
const parseTestSuite = async (visitor, testSuite, isAggregated, reportDir) => {
|
|
81
85
|
const { name, package: packageAttribute, testcase } = testSuite;
|
|
82
86
|
if (!isStringAnyRecordArray(testcase)) {
|
|
83
87
|
return;
|
|
84
88
|
}
|
|
85
89
|
for (const testcaseElement of testcase) {
|
|
86
|
-
await parseTestCase(visitor, { name: ensureString(name), suitePackage: ensureString(packageAttribute) }, testcaseElement, isAggregated);
|
|
90
|
+
await parseTestCase(visitor, { name: ensureString(name), suitePackage: ensureString(packageAttribute) }, testcaseElement, isAggregated, reportDir);
|
|
87
91
|
}
|
|
88
92
|
};
|
|
89
|
-
const parseTestCase = async (visitor, { name: suiteName, suitePackage }, testCase, isAggregated) => {
|
|
93
|
+
const parseTestCase = async (visitor, { name: suiteName, suitePackage }, testCase, isAggregated, reportDir) => {
|
|
90
94
|
const { name: nameAttribute, failure, error, skipped, classname: classNameAttribute, time, "system-out": systemOutAttribute, "system-err": systemErrAttribute, } = testCase;
|
|
91
95
|
const name = ensureString(nameAttribute);
|
|
92
96
|
const className = ensureString(classNameAttribute);
|
|
@@ -100,12 +104,12 @@ const parseTestCase = async (visitor, { name: suiteName, suitePackage }, testCas
|
|
|
100
104
|
status,
|
|
101
105
|
message,
|
|
102
106
|
trace,
|
|
103
|
-
steps: await parseAttachments(visitor, systemOut, systemErr),
|
|
107
|
+
steps: await parseAttachments(visitor, reportDir, systemOut, systemErr),
|
|
104
108
|
labels: convertLabels({ suitePackage, suiteName, className, isAggregated }),
|
|
105
109
|
}, { readerId });
|
|
106
110
|
};
|
|
107
111
|
const convertFullName = (className, name) => (className && name ? `${className}.${name}` : undefined);
|
|
108
|
-
const parseAttachments = async (visitor, systemOut, systemErr) => {
|
|
112
|
+
const parseAttachments = async (visitor, reportDir, systemOut, systemErr) => {
|
|
109
113
|
const attachments = [];
|
|
110
114
|
if (systemOut) {
|
|
111
115
|
attachments.push(await visitPlainTextAttachment(visitor, STDOUT_ATTACHMENT_NAME, systemOut));
|
|
@@ -113,8 +117,46 @@ const parseAttachments = async (visitor, systemOut, systemErr) => {
|
|
|
113
117
|
if (systemErr) {
|
|
114
118
|
attachments.push(await visitPlainTextAttachment(visitor, STDERR_ATTACHMENT_NAME, systemErr));
|
|
115
119
|
}
|
|
120
|
+
for (const [, path] of systemOut?.matchAll(/\[\[ATTACHMENT\|([^\r\n]*?)\]\]/g) ?? []) {
|
|
121
|
+
try {
|
|
122
|
+
const attachmentPath = await resolveAttachmentPath(reportDir, path);
|
|
123
|
+
const content = await readFile(attachmentPath, { flag: constants.O_RDONLY | constants.O_NOFOLLOW });
|
|
124
|
+
const file = new BufferResultFile(content, `${randomUUID()}${extname(path)}`);
|
|
125
|
+
await visitor.visitAttachmentFile(file, { readerId });
|
|
126
|
+
attachments.push({
|
|
127
|
+
type: "attachment",
|
|
128
|
+
name: basename(path),
|
|
129
|
+
contentType: file.getContentType(),
|
|
130
|
+
originalFileName: file.getOriginalFileName(),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.warn(`Unable to read JUnit attachment "${path}":`, error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
116
137
|
return attachments;
|
|
117
138
|
};
|
|
139
|
+
const resolveAttachmentPath = async (reportDir, path) => {
|
|
140
|
+
const root = resolve(reportDir);
|
|
141
|
+
const attachmentPath = resolve(root, path);
|
|
142
|
+
const relativePath = relative(root, attachmentPath);
|
|
143
|
+
if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
144
|
+
throw new Error("Attachment path must be inside the report directory");
|
|
145
|
+
}
|
|
146
|
+
const parts = relativePath.split(sep);
|
|
147
|
+
let currentPath = root;
|
|
148
|
+
for (const [index, part] of parts.entries()) {
|
|
149
|
+
currentPath = join(currentPath, part);
|
|
150
|
+
const stats = await lstat(currentPath);
|
|
151
|
+
if (stats.isSymbolicLink()) {
|
|
152
|
+
throw new Error("Symbolic links are not allowed in attachment paths");
|
|
153
|
+
}
|
|
154
|
+
if (index === parts.length - 1 && !stats.isFile()) {
|
|
155
|
+
throw new Error("Attachment path must refer to a regular file");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return attachmentPath;
|
|
159
|
+
};
|
|
118
160
|
const visitPlainTextAttachment = async (visitor, name, content) => {
|
|
119
161
|
const fileName = randomUUID();
|
|
120
162
|
await visitor.visitAttachmentFile(new BufferResultFile(Buffer.from(content), fileName), { readerId });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allurereport/reader",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"description": "Collection of utilities which helps to process different kind of test results as Allure Results",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"allure",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"lint:fix": "oxlint --import-plugin --fix src test features stories"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@allurereport/core-api": "3.
|
|
30
|
-
"@allurereport/plugin-api": "3.
|
|
31
|
-
"@allurereport/reader-api": "3.
|
|
29
|
+
"@allurereport/core-api": "3.20.0",
|
|
30
|
+
"@allurereport/plugin-api": "3.20.0",
|
|
31
|
+
"@allurereport/reader-api": "3.20.0",
|
|
32
32
|
"fast-xml-parser": "^5.11.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|