Skip to content

Commit 2d17efc

Browse files
authored
feat: cucumber embeddings (#88)
* feat: cucumber embeddings * fix: tests in windows
1 parent 8c9211b commit 2d17efc

File tree

11 files changed

+499
-9
lines changed

11 files changed

+499
-9
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,5 @@ dist
107107
.vscode
108108

109109
.idea/
110+
111+
.testbeats

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "test-results-parser",
3-
"version": "0.2.4",
3+
"version": "0.2.5",
44
"description": "Parse test results from JUnit, TestNG, xUnit, cucumber and many more",
55
"main": "src/index.js",
66
"types": "./src/index.d.ts",

src/helpers/helper.js

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const fs = require('fs');
22
const path = require('path');
3-
const parser = require('fast-xml-parser');
43
const { totalist } = require('totalist/sync');
54
const globrex = require('globrex');
65
const { XMLParser } = require("fast-xml-parser");
@@ -94,8 +93,103 @@ function getMatchingFilePaths(file_path) {
9493
return [file_path];
9594
}
9695

96+
/**
97+
*
98+
* @param {string} value
99+
*/
100+
function decodeIfEncoded(value) {
101+
if (!value) {
102+
return value;
103+
}
104+
try {
105+
if (value.length % 4 !== 0) {
106+
return value;
107+
}
108+
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
109+
if (!base64Regex.test(value)) {
110+
return value;
111+
}
112+
return atob(value);
113+
} catch (error) {
114+
return value;
115+
}
116+
}
117+
118+
/**
119+
*
120+
* @param {string} value
121+
* @returns
122+
*/
123+
function isEncoded(value) {
124+
if (!value) {
125+
return false;
126+
}
127+
try {
128+
if (value.length % 4 !== 0) {
129+
return false;
130+
}
131+
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
132+
if (!base64Regex.test(value)) {
133+
return false;
134+
}
135+
atob(value);
136+
return true;
137+
} catch (error) {
138+
return false;
139+
}
140+
}
141+
142+
/**
143+
*
144+
* @param {string} value
145+
*/
146+
function isFilePath(value) {
147+
try {
148+
fs.statSync(value);
149+
return true;
150+
} catch {
151+
return false;
152+
}
153+
}
154+
155+
/**
156+
*
157+
* @param {string} file_name
158+
* @param {string} file_data
159+
* @param {string} file_type
160+
*/
161+
function saveAttachmentToDisk(file_name, file_data, file_type) {
162+
const folder_path = path.join(process.cwd(), '.testbeats', 'attachments');
163+
fs.mkdirSync(folder_path, { recursive: true });
164+
let data = file_data;
165+
if (isEncoded(file_data)) {
166+
data = Buffer.from(file_data, 'base64');
167+
} else {
168+
return '';
169+
}
170+
171+
const file_path = path.join(folder_path, file_name);
172+
let relative_file_path = path.relative(process.cwd(), file_path);
173+
if (file_type.includes('png')) {
174+
relative_file_path = `${relative_file_path}.png`;
175+
fs.writeFileSync(relative_file_path, data);
176+
} else if (file_type.includes('jpeg')) {
177+
relative_file_path = `${relative_file_path}.jpeg`;
178+
fs.writeFileSync(relative_file_path, data);
179+
} else if (file_type.includes('json')) {
180+
relative_file_path = `${relative_file_path}.json`;
181+
fs.writeFileSync(relative_file_path, data);
182+
} else {
183+
return '';
184+
}
185+
return relative_file_path;
186+
}
187+
97188
module.exports = {
98189
getJsonFromXMLFile,
99190
getMatchingFilePaths,
100-
resolveFilePath
191+
resolveFilePath,
192+
decodeIfEncoded,
193+
isFilePath,
194+
saveAttachmentToDisk
101195
}

src/parsers/base.parser.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ class BaseParser {
99
* @returns
1010
*/
1111
parseStatus(value) {
12-
if (value === 'passed') {
12+
if (value === 'passed' || value === 'PASSED') {
1313
return 'PASS';
1414
}
15-
if (value === 'failed') {
15+
if (value === 'failed' || value === 'FAILED') {
1616
return 'FAIL';
1717
}
18-
if (value === 'skipped') {
18+
if (value === 'skipped' || value === 'SKIPPED') {
1919
return 'SKIP';
2020
}
2121
return 'FAIL';

src/parsers/cucumber.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
const { resolveFilePath } = require('../helpers/helper');
1+
const path = require('path');
2+
const fs = require('fs');
3+
const { resolveFilePath, decodeIfEncoded, isFilePath, saveAttachmentToDisk } = require('../helpers/helper');
24

35
const TestResult = require('../models/TestResult');
46
const TestSuite = require('../models/TestSuite');
57
const TestCase = require('../models/TestCase');
68
const TestStep = require('../models/TestStep');
79
const { BaseParser } = require('./base.parser');
10+
const TestAttachment = require('../models/TestAttachment');
811

912
class CucumberParser extends BaseParser {
1013

@@ -91,6 +94,7 @@ class CucumberParser extends BaseParser {
9194
const { tags, metadata } = this.#getTagsAndMetadata(scenario);
9295
test_case.tags = tags;
9396
test_case.metadata = metadata;
97+
test_case.attachments = this.#getAttachments(scenario.steps);
9498
return test_case;
9599
}
96100

@@ -157,6 +161,55 @@ class CucumberParser extends BaseParser {
157161
return { tags, metadata };
158162
}
159163

164+
/**
165+
*
166+
* @param {import('./cucumber.result').CucumberStep[]} steps
167+
*/
168+
#getAttachments(steps) {
169+
const attachments = [];
170+
const failed_steps = steps.filter(_ => this.parseStatus(_.result.status) === 'FAIL' && _.embeddings && _.embeddings.length > 0);
171+
172+
for (const step of failed_steps) {
173+
for (const embedding of step.embeddings) {
174+
const attachment = this.#getAttachment(step, embedding);
175+
if (attachment) {
176+
attachments.push(attachment);
177+
}
178+
}
179+
}
180+
return attachments;
181+
}
182+
183+
/**
184+
*
185+
* @param {import('./cucumber.result').CucumberStep} step
186+
* @param {import('./cucumber.result').CucumberEmbedding} embedding
187+
*/
188+
#getAttachment(step, embedding) {
189+
try {
190+
const decoded = decodeIfEncoded(embedding.data);
191+
const is_file_path = isFilePath(decoded);
192+
if (is_file_path) {
193+
const attachment = new TestAttachment();
194+
attachment.name = path.parse(decoded).base;
195+
attachment.path = decoded;
196+
return attachment;
197+
} else {
198+
const file_name = step.name.replace(/[^a-zA-Z0-9]/g, '_') + '-' + Date.now();
199+
const file_path = saveAttachmentToDisk(file_name, embedding.data, embedding.mime_type);
200+
if (!file_path) {
201+
return null;
202+
}
203+
const attachment = new TestAttachment();
204+
attachment.name = path.parse(file_path).base;
205+
attachment.path = file_path;
206+
return attachment;
207+
}
208+
} catch (e) {
209+
return null;
210+
}
211+
}
212+
160213
}
161214

162215
function parse(file) {

src/parsers/cucumber.result.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ export type CucumberStep = {
2323
name: string;
2424
match: CucumberMatch;
2525
result: CucumberResult;
26+
embeddings?: CucumberEmbedding[];
2627
};
2728

29+
export type CucumberEmbedding = {
30+
data: string;
31+
mime_type: string;
32+
}
33+
2834
export type CucumberTag = {
2935
name: string;
3036
line: number;
371 KB
Loading

tests/data/cucumber/test-with-attachments.json

Lines changed: 117 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
[
2+
{
3+
"description": "Verify calculator functionalities",
4+
"elements": [
5+
{
6+
"description": "",
7+
"id": "addition;addition-of-two-numbers",
8+
"keyword": "Scenario",
9+
"line": 5,
10+
"name": "Addition of two numbers",
11+
"steps": [
12+
{
13+
"arguments": [],
14+
"keyword": "Given ",
15+
"line": 6,
16+
"name": "I have number 6 in calculator",
17+
"match": {
18+
"location": "features\\support\\steps.js:5"
19+
},
20+
"result": {
21+
"status": "passed",
22+
"duration": 1211400
23+
}
24+
},
25+
{
26+
"arguments": [],
27+
"keyword": "When ",
28+
"line": 7,
29+
"name": "I entered number 7",
30+
"match": {
31+
"location": "features\\support\\steps.js:9"
32+
},
33+
"result": {
34+
"status": "passed",
35+
"duration": 136500
36+
}
37+
},
38+
{
39+
"arguments": [],
40+
"keyword": "Then ",
41+
"line": 8,
42+
"name": "I should see result 13",
43+
"match": {
44+
"location": "features\\support\\steps.js:13"
45+
},
46+
"result": {
47+
"status": "failed",
48+
"duration": 1330499,
49+
"error_message": "AssertionError [ERR_ASSERTION]: 13 == 14\n + expected - actual\n\n -13\n +14\n\n at CustomWorld.<anonymous> (D:\\workspace\\nodejs\\cc-tests\\features\\support\\steps.js:18:12)"
50+
},
51+
"embeddings": [
52+
{
53+
"data": "some-invalid-data-path",
54+
"mime_type": "image/png"
55+
},
56+
{
57+
"data": "tests/data/attachments/screenshot.png",
58+
"mime_type": "image/png"
59+
},
60+
{
61+
"data": "ZGF0YQ==",
62+
"mime_type": ""
63+
}
64+
]
65+
},
66+
{
67+
"arguments": [],
68+
"keyword": "And ",
69+
"line": 49,
70+
"name": "I close the test",
71+
"match": {
72+
"location": "features\\support\\steps.js:14"
73+
},
74+
"result": {
75+
"status": "skipped",
76+
"duration": 0
77+
}
78+
}
79+
],
80+
"tags": [
81+
{
82+
"name": "@green",
83+
"line": 4
84+
},
85+
{
86+
"name": "@fast",
87+
"line": 4
88+
},
89+
{
90+
"name": "@testCase=1234",
91+
"line": 4
92+
}
93+
],
94+
"type": "scenario"
95+
}
96+
],
97+
"id": "addition",
98+
"line": 1,
99+
"keyword": "Feature",
100+
"name": "Addition",
101+
"tags": [
102+
{
103+
"name": "@blue",
104+
"line": 4
105+
},
106+
{
107+
"name": "@slow",
108+
"line": 4
109+
},
110+
{
111+
"name": "@suite=1234",
112+
"line": 4
113+
}
114+
],
115+
"uri": "features\\sample.feature"
116+
}
117+
]

0 commit comments

Comments
 (0)